Skip to content
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

Merged

Conversation

marta-lokhova
Copy link
Contributor

@marta-lokhova marta-lokhova commented Mar 21, 2024

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

@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch 7 times, most recently from bf80d96 to fc2cb17 Compare March 26, 2024 04:11
@marta-lokhova marta-lokhova mentioned this pull request Mar 29, 2024
6 tasks
Copy link
Contributor

@bboston7 bboston7 left a 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.

src/main/ApplicationImpl.cpp Outdated Show resolved Hide resolved
src/main/Config.cpp Outdated Show resolved Hide resolved
src/overlay/OverlayManagerImpl.h Outdated Show resolved Hide resolved
src/overlay/OverlayManagerImpl.cpp Outdated Show resolved Hide resolved
src/overlay/FlowControl.cpp Outdated Show resolved Hide resolved
src/overlay/FlowControlCapacity.h Outdated Show resolved Hide resolved
src/overlay/Peer.cpp Outdated Show resolved Hide resolved
src/overlay/TCPPeer.h Outdated Show resolved Hide resolved
src/overlay/TCPPeer.cpp Outdated Show resolved Hide resolved
@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch from e943fb8 to 01f6354 Compare April 11, 2024 03:51
@marta-lokhova marta-lokhova marked this pull request as ready for review April 11, 2024 05:14
@marta-lokhova marta-lokhova changed the title add an option to run overlay operation in the background add an option to run overlay operations in the background Apr 11, 2024
@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch from 32be063 to e53473d Compare April 12, 2024 23:40
Copy link
Contributor

@SirTyson SirTyson left a 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 :).

// start with 1 thread for simplicity, as there isn't enough overlay work to
// consume one whole CPU
, mHighPriorityIOContext(
!!mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: !!

@@ -143,6 +151,8 @@ ApplicationImpl::ApplicationImpl(VirtualClock& clock, Config const& cfg)
}};
mWorkerThreads.emplace_back(std::move(thread));
}

mOverlayThread = std::thread{[this]() { mHighPriorityIOContext.run(); }};
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, done.

, mHighPriorityIOContext(
!!mConfig.EXPERIMENTAL_BACKGROUND_OVERLAY_PROCESSING)
, mHighPriorityWork(
std::make_unique<asio::io_context::work>(mHighPriorityIOContext))
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, updated.

Application& mApp;
OverlayMetrics& mOverlayMetrics;
OverlayManager& mOverlayManager;
Config const mConfig;
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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.

@@ -346,6 +366,8 @@ OverlayManagerImpl::start()
OverlayManager::AdjustedFlowControlConfig
OverlayManagerImpl::getFlowControlBytesConfig() const
{
std::lock_guard<std::recursive_mutex> lock(mOverlayMutex);
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -101,6 +101,7 @@ Peer::pointer
OverlayManagerImpl::PeersList::byAddress(PeerBareAddress const& address) const
{
ZoneScoped;

Copy link
Contributor

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.

@@ -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,
Copy link
Contributor

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.

Copy link
Contributor Author

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());
Copy link
Contributor

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

@marta-lokhova marta-lokhova self-assigned this Apr 16, 2024
@bboston7 bboston7 mentioned this pull request Apr 22, 2024
13 tasks
@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch 2 times, most recently from f20f9e4 to b198d93 Compare May 9, 2024 00:54
src/main/Config.cpp Outdated Show resolved Hide resolved
@marta-lokhova
Copy link
Contributor Author

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:

  • Updates to peer API: explicitly require most public functions to be main-thread only, audit the rest for thread-safety.
  • Synchronize peer state with an explicit fine-grained lock, remove global locking to make it easier to reason about synchronization. As part of this, add getters and setters for Peer’s state, that all require a lock guard, hinting to the caller that the lock is needed. Hide any other access to state outside of peer. If callers need to access it, they can pass a lambda and let Peer handle the synchronization.
  • Flow control: remove dependency on Peer completely by removing the send callback. The goal is to prevent flow control from ever calling any method in Peer. This is needed since we have two locks now: Peer’s state lock and flow control lock, they have to be ordered, so the order of locking should always be peer lock followed by flow control. This way we can guarantee the two new locks never deadlock.
  • Get rid of any locking at the peer lists layer: the problem with synchronizing peer lists is that it bleeds into too many subsystems past overlay, so it's hard to reason about and review. The updated design avoids the need to synchronize peer lists completely: only the main thread maintains peer lists and officially "drops" peers. The background thread asynchronously watches for any state changes, and closes the socket. Note that this pattern already exists in master today, where we delay the actual socket shutdown on the main thread, while the Peer is dropped from the OverlayManager point of view, so I'm just re-using it but in the background.

@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch from b198d93 to fb9b38e Compare May 10, 2024 18:58
Copy link
Contributor

@SirTyson SirTyson left a 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!

: nullptr)
, mEvictionIOContext(
mConfig.EXPERIMENTAL_BACKGROUND_EVICTION_SCAN
? std::make_unique<asio::io_context>(DEFAULT_THREAD_COUNT)
Copy link
Contributor

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(); }};
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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));
Copy link
Contributor

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;
Copy link
Contributor

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

Copy link
Contributor Author

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.

@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Missing .

auto result = weak.lock();
if (!result)
{
// If peer was rejected was reasons like no pending slots
Copy link
Contributor

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 Show resolved Hide resolved
return mSocketShutdownScheduled;
}
void
scheduleSocketShutdown(bool value)
Copy link
Contributor

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.

src/overlay/TCPPeer.cpp Show resolved Hide resolved
Copy link
Contributor

@bboston7 bboston7 left a 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.

src/overlay/Peer.h Outdated Show resolved Hide resolved
src/overlay/Peer.h Show resolved Hide resolved
src/overlay/Peer.h Outdated Show resolved Hide resolved
src/overlay/Peer.cpp Outdated Show resolved Hide resolved
src/overlay/Peer.cpp Show resolved Hide resolved
src/overlay/TCPPeer.cpp Outdated Show resolved Hide resolved
src/overlay/test/LoopbackPeer.cpp Outdated Show resolved Hide resolved
@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch 2 times, most recently from 995461a to 70c649d Compare May 17, 2024 20:57
marta-lokhova and others added 22 commits May 29, 2024 16:15
@marta-lokhova marta-lokhova force-pushed the parallelize_ov_initial_without_hashing branch from 2904683 to 894ef3b Compare May 29, 2024 23:21
latobarita added a commit that referenced this pull request May 29, 2024
…thout_hashing

add an option to run overlay operations in the background

Reviewed-by: graydon
@graydon
Copy link
Contributor

graydon commented May 29, 2024

r+ 894ef3b

@graydon
Copy link
Contributor

graydon commented May 29, 2024

@latobarita: retry

latobarita added a commit that referenced this pull request May 30, 2024
…thout_hashing

add an option to run overlay operations in the background

Reviewed-by: graydon
@graydon
Copy link
Contributor

graydon commented May 30, 2024

@latobarita: retry

@latobarita latobarita merged commit c891524 into stellar:master May 30, 2024
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants