-
Notifications
You must be signed in to change notification settings - Fork 969
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
add an option to run overlay operations in the background #4258
add an option to run overlay operations in the background #4258
Conversation
bf80d96
to
fc2cb17
Compare
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.
This is an exciting change! It was fun to think about possible interleavings with the backgrounded overlay thread.
e943fb8
to
01f6354
Compare
32be063
to
e53473d
Compare
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.
Thanks for putting this together! Definitely a tricky change but I'm looking forward to parallel core! That being said, I don't think we should merge this as is. I haven't finished reviewing this quite yet, but I got particularly stuck on the Peer
classes.
I think currently the Peer
interface is too difficult to reason about and is error prone. For example, we have two overloads of recvMessage
, one that asserts it's the main thread and the other which asserts it's the background thread. We also have multiple ways to get to the same object depending on if a given function is calling from a main thread or background thread context. For example, we have a const copy of Config
that the background thread functions use, while main thread functions just call mApp.getConfig()
. It's fairly implicit which functions should be able to access mApp
and which functions should not. Finally, there's some functions used in both a main thread and background context, like drop
, making the boundary even muddier.
Personally, I don't think we should merge this change without some stronger division between threadsafe and non-threadsafe functions in Peer
. This is my first venture into the guts of overlay, so some of the confusion may be in part because I'm learning this part of the codebase. That being said, while unfortunately it's a fairly significant amount of work, I feel like a refactor of Peer
may be necessary. If we decide not to refactor after all that's fine, but I will need to study a bit more and do a second pass to finish the review :).
src/main/ApplicationImpl.cpp
Outdated
// start with 1 thread for simplicity, as there isn't enough overlay work to | ||
// consume one whole CPU | ||
, mHighPriorityIOContext( | ||
!!mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING) |
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.
Nit: !!
src/main/ApplicationImpl.cpp
Outdated
@@ -143,6 +151,8 @@ ApplicationImpl::ApplicationImpl(VirtualClock& clock, Config const& cfg) | |||
}}; | |||
mWorkerThreads.emplace_back(std::move(thread)); | |||
} | |||
|
|||
mOverlayThread = std::thread{[this]() { mHighPriorityIOContext.run(); }}; |
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 should be protected behind the mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING
flag.
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.
Good call, done.
src/main/ApplicationImpl.cpp
Outdated
, mHighPriorityIOContext( | ||
!!mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING) | ||
, mHighPriorityWork( | ||
std::make_unique<asio::io_context::work>(mHighPriorityIOContext)) |
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.
Should mHighPriorityWork
be nullptr
iff !mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING
?
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.
Yep, updated.
src/overlay/FlowControl.h
Outdated
Application& mApp; | ||
OverlayMetrics& mOverlayMetrics; | ||
OverlayManager& mOverlayManager; | ||
Config const mConfig; |
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 not Config const&
here? We have references to "main thread" state already with mOverlayMetrics
so I don't see why we need to copy here.
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.
In general, in a multi-threaded environment, I prefer a copy to a reference to avoid auditing the code for thread-safety and prevent footguns. This works when performance isn't an issue of course: in this case I think it's fine since Config is a tiny const object stored in each peer.
I'd ideally like to do the same for OverlayMetrics (e.g. deal with copies and do message passing of sorts), but that's more involved, so maybe as a follow-up.
@@ -54,7 +72,12 @@ class FlowControl | |||
std::shared_ptr<FlowControlCapacity> mFlowControlCapacity; | |||
std::shared_ptr<FlowControlByteCapacity> mFlowControlBytesCapacity; | |||
|
|||
Application& mApp; | |||
OverlayMetrics& mOverlayMetrics; |
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 might be worth auditing medida
for thread safety. Unfortunately, the library is like 12 years old, the docs page is 404, and there are no comments, so I don't know for sure if it's thread safe. Doing a pass through it looks like all relevant medida
state is protected with std::atomic
or a mutex so we should be ok, but it might warrant a closer look.
src/overlay/OverlayManagerImpl.cpp
Outdated
@@ -346,6 +366,8 @@ OverlayManagerImpl::start() | |||
OverlayManager::AdjustedFlowControlConfig | |||
OverlayManagerImpl::getFlowControlBytesConfig() const | |||
{ | |||
std::lock_guard<std::recursive_mutex> lock(mOverlayMutex); |
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 do we need the lock here? It looks like the only value here that can change is HerderImpl::mMaxTxSize
. That value is modified in HerderImpl::start
and HerderImpl::maybeHandleUpgrade
, neither of which acquire mOverlayMutex
so currently this lock is useless. I think we need to acquire the mutex in maybeHandleUpgrade
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.
We should make OverlayManagerImpl::resolvePeers(std::vector<string> const& peers)
static. It currently doesn't acquire the lock (and it doesn't need to), but we should make this static to further protect against unsafe member access.
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 agree it should be static, but given that data safety isn't a concern here anymore (the overlay lock was removed in favor of more granular locking), let's make this change later as to not further expand the scope of this PR.
src/overlay/OverlayManagerImpl.cpp
Outdated
@@ -101,6 +101,7 @@ Peer::pointer | |||
OverlayManagerImpl::PeersList::byAddress(PeerBareAddress const& address) const | |||
{ | |||
ZoneScoped; | |||
|
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.
While it looks like all the calls into PeerList
are properly guarded by the overlay lock, I'd still like the PeerList
functions to acquire the lock too just to be on the safe side.
src/overlay/TCPPeer.h
Outdated
@@ -102,6 +119,8 @@ class TCPPeer : public Peer | |||
|
|||
virtual ~TCPPeer(); | |||
|
|||
// Thread-safe peer drop (can be initiated from either main or overlay | |||
// thread) | |||
virtual void drop(std::string const& reason, DropDirection dropDirection, |
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 drop
called from the Overlay thread? I'm not quite sure I understand the main thread-background thread handoff when it comes to peer management. It seems like the mainthread-background cutoff should be main thread manages connections, background handles message in/out. This seems to muddy the water a bit.
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.
Drop can be initiated from either thread, because there are different reasons for dropping a connection. E.g. we may encounter a super low-level ASIO error from background, or main thread deciding that peer doesn't follow the protocol at that (higher) layer of abstraction. That being said, in the updated version there is no need to synchronize peer lists, and the responsibilities are separated such that peer list is always handled on main, while socket shutdown is handled in the background.
@@ -281,6 +351,7 @@ FlowControl::endMessageProcessing(StellarMessage const& msg, | |||
std::weak_ptr<Peer> peer) | |||
{ | |||
ZoneScoped; | |||
releaseAssert(threadIsMain()); |
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.
To be on the safe side, I'd like to acquire the overlay lock in beginMessageProcessing
and endMessageProcessing
f20f9e4
to
b198d93
Compare
Thanks all for the feedback! I pushed a refreshed version of the PR. Working through a few remaining items, like new unit tests and a small remaining todo to make peer's string address thread-safe, but here's an overview of the updates:
|
b198d93
to
fb9b38e
Compare
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.
Thanks for doing all the refactoring work, this is significantly more clear and easier to follow! I left a few nit pics and and questions, and still probably want to walk through it together to make sure I understand everything before signing off, but it's looking good!
src/main/ApplicationImpl.cpp
Outdated
: nullptr) | ||
, mEvictionIOContext( | ||
mConfig.EXPERIMENTAL_BACKGROUND_EVICTION_SCAN | ||
? std::make_unique<asio::io_context>(DEFAULT_THREAD_COUNT) |
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 should stay hardcoded to 1. DEFAULT_THREAD_COUNT
feels to me a lower bound value that might increase over time as we become more parallel and get beefier machines. For overlay, DEFAULT_THREAD_COUNT
values of 2 or 4 would make sense in the future. However, we will only every need a single thread for background eviction, as it's a single task that is not parallizable itself, so I'd like to keep the hardcoded 1.
|
||
if (mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING) | ||
{ | ||
mOverlayThread = std::thread{[this]() { mOverlayIOContext->run(); }}; |
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.
Do we need to set priority here? Or is it implicit that these threads maintain the same priority as main thread? If implicit, a comment would be nice.
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.
As overlay processing is fairly high-priority and time-sensitive, I didn't modify priority. Added a comment though.
@@ -981,6 +1008,13 @@ ApplicationImpl::joinAllThreads() | |||
} | |||
|
|||
LOG_DEBUG(DEFAULT_LOG, "Joined all {} threads", mWorkerThreads.size()); | |||
if (mOverlayThread) |
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.
Nit: Change eviction thread logs to INFO for consistency.
mOverlayThread->join(); | ||
} | ||
|
||
LOG_INFO(DEFAULT_LOG, "Joined all {} threads", (mWorkerThreads.size() + 1)); |
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.
You should remove LOG_DEBUG(DEFAULT_LOG, "Joined all {} threads", mWorkerThreads.size());
, as right now we print two identical thread join messages with different numbers.
// Since MsgCapacityTracker is passed around between thread and stored | ||
// by ASIO in lambdas, do not rely on RAII to trigger completion. | ||
// Instead, explicitly call `finish`. | ||
void finish(); | ||
}; | ||
|
||
OverlayAppConnector mAppConnector; | ||
|
||
Hash const mNetworkID; | ||
std::shared_ptr<FlowControl> mFlowControl; |
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.
std::shared_ptr<FlowControl> const
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.
Good idea! there are some test dependencies sadly, which override mFlowControl. I can fix this in a follow-up though after this PR lands.
src/overlay/Floodgate.cpp
Outdated
@@ -137,6 +139,9 @@ Floodgate::broadcast(std::shared_ptr<StellarMessage const> msg, | |||
mSendFromBroadcast.Mark(); | |||
std::weak_ptr<Peer> weak( | |||
std::static_pointer_cast<Peer>(peer.second)); | |||
// This is an async operation, and peer might get dropped by the | |||
// time we actually try to send the message This is fine, as |
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.
Nit: Missing .
src/overlay/TCPPeer.cpp
Outdated
auto result = weak.lock(); | ||
if (!result) | ||
{ | ||
// If peer was rejected was reasons like no pending slots |
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.
Typo: for reasons
src/overlay/TCPPeer.h
Outdated
return mSocketShutdownScheduled; | ||
} | ||
void | ||
scheduleSocketShutdown(bool value) |
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.
Remove bool value
. This should only every be called once anyway and true
is the only valid value, so remove parameter.
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.
This is looking great! I just had a few small comments.
995461a
to
70c649d
Compare
…s are called on main
…ibility of deadlocks via lock ordering
…ted peers to Dropped for consistency
…ket creation safety
2904683
to
894ef3b
Compare
…thout_hashing add an option to run overlay operations in the background Reviewed-by: graydon
r+ 894ef3b |
@latobarita: retry |
…thout_hashing add an option to run overlay operations in the background Reviewed-by: graydon
@latobarita: retry |
First stab at running some of overlay processing in the background. At a high level, the change splits processing of raw inbound/outbound data and higher level business logic of the application. The rationale for the split is that this way we actually get a more-or-less clear cut of responsibilities between overlay and the rest of the application (SCP, tx queue, etc).
In this change the following is moved to the background: socket reads/writes, XDR decoding, HMAC verification, signature verification of SCP messages, tx hashing, flow control outbound broadcasting. Now that we have BL snapshotting available, we can go further and do transaction validation in the background, and possibly exit early to avoid additional burden on the scheduler and the tx queue. For the sake of doing things iteratively, I'd rather do that in a subsequent PR as this change is already quite big.
There are a couple of design choices in this PR that attempt to de-risk it as much as possible. For simplicity, there's a single "overlay lock" and a single "overlay thread". Single lock simplifies synchronization and removes a whole class of deadlock bugs. A single thread seems good enough for now, as our traffic patterns are fairly heavily throttled, so most of the time we're actually idle, but periodically we receive bursts of data that main thread needs to process as fast as possible. So the value of many threads is not clear at the moment given that we're still bottle-necked by the main thread, which runs SCP that keeps the node in sync.
The code is structured such that everything before we get to a valid StellarMessage is managed by the background thread, and the business logic is done on the main thread. When main thread is done processing, some of the outbound processing, like flow control + asio queuing, is also done in the background.
I wanted to get the initial version out to get review going and kick-off discussions on de-risking these changes further, as well as minimizing the impact when the experimental flag is off.
Resolves #4320
Resolves #4309