Dedup and RAII-fy the creation of a copy of CConnman::vNodes (p2p)

https://github.com/bitcoin/bitcoin/pull/21943

Host: jnewbery  -  PR author: vasild

The PR branch HEAD was 99c1af5a8f at the time of this review club meeting.

Notes

  • CConnman is the class in net that opens, maintains and closes connections to peers, manages sockets, and reads/writes messages on the network.

  • Within CConnman, we maintain vNodes, a vector of connections to other nodes. That vector is updated and accessed by various nodes, including:

    • the socket handler thread, which is responsible for reading data from the sockets into receive buffers, and also for accepting new incoming connections.
    • the open connections thread, which is responsible for opening new connections to peers on the network.
    • the message handler thread, which is responsible for reading messages from the receive buffer and passing them up to net_processing.
  • Since the vector can be updated by multiple threads, it is guarded by a mutex called cs_vNodes.

  • For operations that are done on each connection in turn (e.g. reading from each socket in the socket handler thread, or passing messages to net_processing in the message handler thread), the common pattern is to:

    • lock cs_vNodes
    • make a copy of the vNodes vector
    • for each CNode object, increment an internal nRefCount atomic counter.
    • release cs_vNodes
    • operate on each of the CNode objects in the vNodes copy in turn
    • decrement the nRefCount counter for each CNode
  • This PR proposes to extract that pattern into an RAII object called NodesSnapshot. It also changes the order of some logic in the socket handler thread. The motivation is to reduce lock contentions.

Questions

  1. Did you review the PR? Concept ACK, approach ACK, tested ACK, or NACK?

  2. What are the advantages of using RAII?

  3. The CNode object contains a member called nRefCount. This is rather similar to the reference counter in a shared pointer. Why do you think we use raw pointers to store CNode objects in vNodes instead of shared pointers?

  4. The destructor of the NodesSnapshot class calls Release() on each CNode object in turn. It does this without locking the cs_vNodes mutex. Is that ok? Why/why not?

  5. The socket handler thread contains a while() loop that runs for the entire duration of bitcoind’s runtime (interruptNet is only toggled to false during the shutdown sequence). For that loop not to be ‘busy’ and peg the CPU, there should be a sleep somewhere in the loop. Where is that sleep for the socket handler thread?

  6. The second commit (net: don’t process newly accepted nodes in SocketHandler()) moves the logic for accepting new connections below where the nodes snapshot is taken. What are the consequences of doing that?

  7. It has been claimed that this PR reduces lock contention. How would you verify or test that?

Meeting Log

  117:00 <jnewbery> #startmeeting
  217:00 <jnewbery> Hi folks! Welcome to Bitcoin Core PR Review Club.
  317:00 <dopedsilicon> Hi
  417:01 <jnewbery> Thank you for coming to learn more about the Bitcoin protocol, Bitcoin Core, and the review process.
  517:01 <jnewbery> Feel free to say hi to let everyone know you're here.
  617:01 <urraca> hi
  717:01 <emzy> hi
  817:01 <jnewbery> Is anyone here for the first time?
  917:01 <urraca> yea, just finished the chaincode seminar
 1017:02 <jnewbery> urraca: that's great! Welcome :)
 1117:02 <jnewbery> Notes and questions are in the normal place: https://bitcoincore.reviews/21943
 1217:02 <larryruane> hi
 1317:02 <jnewbery> It's a pretty small refactor PR this week. Who had a chance to read the review club notes / review the PR? (y/n)
 1417:03 <larryruane> y (mostly)
 1517:03 <emzy> n (just read the writeup)
 1617:04 <urraca> n, same
 1717:04 <dopedsilicon> n
 1817:05 <jnewbery> that's ok. I understand most people are doing this in their free time, so no worries if you didn't have time to review the code this week. Feel free to ask any questions as we go through it
 1917:05 <jnewbery> ok, first question. Did you review the PR? Concept ACK, approach ACK, tested ACK, or NACK?
 2017:06 <jnewbery> larryruane: what were your impressions?
 2117:06 <larryruane> concept and approach ACKs (i'll update the PR in a bit)
 2217:06 <jnewbery> :) excellent
 2317:06 <larryruane> I think it's great, takes advantage of a deeper understanding of c++ (actually i wouldn't have thought of it!)
 2417:07 <jnewbery> Can anyone give a brief summary of what the PR is doing?
 2517:08 <emzy> reduce lock contentions.
 2617:09 <jnewbery> There are two parts to the PR. It's using RAII for copying the list of nodes in connman, and also changing the ordering of operations in the socket handler thread.
 2717:09 <emzy> The other one may be simplification.
 2817:09 <jnewbery> Which leads us to the next questions. What is RAII, and what are the advantages of using it?
 2917:10 <larryruane> There are a bunch of places where we need to operate on every node, but while doing that (across multiple nodes), nodes can be added and deleted - this is why the pre-PR code makes a copy ... this PR makes making that copy safer and more systematic
 3017:10 <jnewbery> emzy: right, the motivation (or at least partial motivation) is reducing lock contention
 3117:10 <larryruane> resource acquisition and initialization (rolls right off the tongue)
 3217:11 <jnewbery> larryruane: right, this is about where we're doing operations on all of our peers in a thread safe way
 3317:11 <jnewbery> larryruane: Close. It's actually "resource acquisition *IS* initialization"
 3417:12 <jnewbery> Anyone want to expand a bit on what that means?
 3517:13 <larryruane> thanks, so the very simple case of just declaring an object (as a local variable let's say), that's instantiating the resource (the object), and since its constructor runs, initializing it at the same time? Or is there more to it?
 3617:13 <larryruane> instantiation == acquistion
 3717:14 <larryruane> as opposed to if you declare an integer `int x;` that doesn't do any initialization, it could contain any garbage
 3817:14 <glozow> hi
 3917:15 <jnewbery> larryruane: yes, that's exactly right. It uses the fact that in C++, whenever an object is declared, its constructor is called, and whenever that object goes out of scope, its destructor is called
 4017:16 <jnewbery> that means that we can use the object's lifetime to control the acquisition and release of resources, such as memory, refcounts, etc
 4117:17 <larryruane> What I've struggled with a bit at time is if there's a typedef to an `int64_t` let's say, it may not be clear from reading a declaration of a variable of that type whether it's being initialized (this is an object (class instance) versus just an int), without looking up the type
 4217:18 <jnewbery> the point is more about the release of the resource at the end of the object's lifetime. If we manually acquire the resource in a preocedural way, and then release the resource at the end of the function or whatever, we need to always check that there's no other way to leave that function (eg through an early exit or exception throw) that would miss that release and therefore leak the
 4317:18 <jnewbery> resource
 4417:19 <larryruane> thanks, yes, C code is typically riddled with such memory leak bugs
 4517:19 <jnewbery> larryruane: yes, the rules about default initialization are quite complicated!
 4617:20 <larryruane> I'm sorry to belabor the point, we can go on, but when I read `footype x;` I wonder if it should be `footype x{0}` or if the initialization is being taken care of
 4717:21 <sipa> something i idly wonder about now: what does "int x{};" do?
 4817:21 <jnewbery> larryruane: right, that depends on context. Is it in a function? A global? A member of a class?
 4917:23 <jnewbery> larryruane: it's a bit of a rabbithole, but cppreference.com and stackoverflow should be able to answer the question!
 5017:23 <jnewbery> perhaps we should move on
 5117:23 <jnewbery> The CNode object contains a member called nRefCount. This is rather similar to the reference counter in a shared pointer. Why do you think we use raw pointers to store CNode objects in vNodes instead of shared pointers?
 5217:24 <jnewbery> sipa: I also idly wonder that, and wouldn't want to hazard a guess
 5317:25 <larryruane> just a guess, could it be because of the way CNodes are allocated, and so changing to use shared pointers would be too large of a change?
 5417:27 <jnewbery> larryruane: there's nothing very special about the way CNodes are allocated. `new CNode` is called (eg here: https://github.com/bitcoin/bitcoin/blob/a9f642870849dcbfe32632fd6614804be61ab40c/src/net.cpp#L488)
 5517:28 <jnewbery> memory allocation would be very similar if it was a shared pointer and std::make_shared was used (there would also be a control block allocated, but that's not really important)
 5617:29 <jnewbery> I can guess two possible reasons that raw pointers are used
 5717:29 <jnewbery> 1. This code was written long, long ago, before we moved to c++11 and had smart pointers
 5817:29 <sipa> it dates from 2009 iirc
 5917:30 <jnewbery> 2. we want finer control over the lifetime of the CNode object than just deleting the final shared pointer to the object and it's gone
 6017:31 <jnewbery> and maybe 2.5: we could move to shared pointers, but it would require careful thinking about whether 2 is an issue, and there are higher priority things to work on
 6117:31 <jnewbery> in any case, we have a manual refcount in the CNode object that acts somewhat like the refcount of a shared pointer
 6217:32 <jnewbery> as long as there are other threads holding a pointer to the object, we shouldn't delete it
 6317:32 <jnewbery> any questions about that or shall we move on to the next question?
 6417:33 <jnewbery> The destructor of the NodesSnapshot class calls Release() on each CNode object in turn. It does this without locking the cs_vNodes mutex. Is that ok? Why/why not?
 6517:35 <larryruane> I think it's okay because `nRefCount` is atomic ... ?
 6617:35 <jnewbery> larryruane: I agree!
 6717:36 <jnewbery> There's some good discussion here: https://github.com/bitcoin/bitcoin/pull/21943#discussion_r632974904
 6817:37 <jnewbery> ok, next question. The socket handler thread contains a while() loop that runs for the entire duration of bitcoind’s runtime (interruptNet is only toggled to false during the shutdown sequence). For that loop not to be ‘busy’ and peg the CPU, there should be a sleep somewhere in the loop. Where is that sleep for the socket handler thread?
 6917:38 <larryruane> it has to be in `SocketHandler()` ... but I don't see where within that function it's blocking (i don't know socket stuff very well)
 7017:39 <jnewbery> Yeah, it's actually quite difficult to find since it's buried deep in the SocketEvents() function: https://github.com/bitcoin/bitcoin/blob/a9f642870849dcbfe32632fd6614804be61ab40c/src/net.cpp#L1386
 7117:40 <jnewbery> (or here if you're using select: https://github.com/bitcoin/bitcoin/blob/a9f642870849dcbfe32632fd6614804be61ab40c/src/net.cpp#L1428)
 7217:41 <jnewbery> those interruptNet.sleep_for() mean that we'll wait for events on the socket rather than just continually looping
 7317:42 <larryruane> hmm, the code at the first link (line 1386) doesn't look as expected -- it takes 3 sets as arguments, but seems to be called with `snap.Nodes()` as the first argument
 7417:43 <jnewbery> larryruane: that link was to the master branch
 7517:43 <larryruane> (actually same for both) .. OH i see! got it
 7617:44 <jnewbery> Next question: The second commit (net: don’t process newly accepted nodes in SocketHandler()) moves the logic for accepting new connections below where the nodes snapshot is taken. What are the consequences of doing that?
 7717:47 <jnewbery> The commit log claims "it is certain that newly accepted nodes' sockets will not be
 7817:47 <jnewbery> reported as ready for IO by `CConnman::SocketEvents()` (because it ran
 7917:47 <jnewbery> before they existed). Thus checking whether the socket of a newly
 8017:47 <jnewbery> accepted node is in the recv/send/error set is a noop."
 8117:49 <larryruane> so it seems like a performance improvement, but then the second point (in that comment) seems to prevent incorrect behavior (disconnect)
 8217:49 <jnewbery> I had to review this quite carefully to convince myself that it was ok. It's a little difficult to get your head around the ordering of things in the socket handler loop, and when the thread is sleeping/waiting for events on the socket
 8317:50 <jnewbery> larryruane: right, the second part of the commit log is explaining why the change is safe
 8417:51 <jnewbery> ok, final question: It has been claimed that this PR reduces lock contention. How would you verify or test that?
 8517:51 <larryruane> maybe timing the fuzz testing would do it (but I haven't looked at the fuzz tests for this)
 8617:52 <jnewbery> larryruane: I think that would be very noisy
 8717:52 <larryruane> seems like probably in normal operation it wouldn't be enough of a difference to notice
 8817:53 <jnewbery> We actually have logging for lock contentions. If you grep for "BCLog::LOCK", you'll see where it's used
 8917:53 <larryruane> oh very cool TIL
 9017:53 <jnewbery> https://github.com/bitcoin/bitcoin/blob/71a85fbd09b5a450edc53a8ba4131f32e7136ca7/src/sync.h#L140
 9117:54 <jnewbery> I haven't actually used it myself. It was added recently in https://github.com/bitcoin/bitcoin/pull/22736
 9217:54 <jnewbery> ok, 5 minutes left. Any questions?
 9317:55 <larryruane> i did have one, just a sec to type it out...
 9417:55 <jnewbery> larryruane: sure!
 9517:55 <larryruane> ok right here, https://github.com/bitcoin/bitcoin/blob/927586990/src/net.cpp#L1275
 9617:56 <larryruane> If the condition is false, then I expect that later WHEN the refcount decrements to zero, we would be doing these same things (the `remove` and `DeleteNode()`)
 9717:56 <larryruane> I don't see that, but I'm probably just missing it
 9817:57 <larryruane> (that's the pattern i'm used to seeing on other projects i've worked on)
 9917:58 <jnewbery> larryruane: right, when the refcount drops to zero, we delete the object. If not, then we continue and try again on the next loop of thread socket handler
10017:58 <larryruane> ok thanks i'll have to study that a bit .. that's all from me!
10117:59 <jnewbery> ok, thanks everyone!
10217:59 <jnewbery> #endmeeting