Skip to content

Commit b59f278

Browse files
committed
Merge #18017: txmempool: split epoch logic into class
fd6580e [refactor] txmempool: split epoch logic into class (Anthony Towns) Pull request description: Splits the epoch logic introduced in #17925 into a separate class. Uses clang's thread safety annotations and encapsulates the data more strongly to reduce chances of bugs from API misuse. ACKs for top commit: jonatack: ACK fd6580e using clang thread safety annotations looks like a very good idea, and the encapsulation this change adds should improve robustness (and possible unit test-ability) of the code. Verified that changing some of the locking duly provoked build-time warnings with Clang 9 on Debian and that small changes in the new `Epoch` class were covered by failing functional test assertions in `mempool_updatefromblock.py`, `mempool_resurrect.py`, and `mempool_reorg.py` hebasto: re-ACK fd6580e, since my [previous](bitcoin/bitcoin#18017 (review)) review: Tree-SHA512: 7004623faa02b56639aa05ab7a078320a6d8d54ec62d8022876221e33f350f47df51ddff056c0de5be798f8eb39b5c03c2d3f035698555d70abc218e950f2f8c
2 parents 587c986 + fd6580e commit b59f278

File tree

4 files changed

+107
-63
lines changed

4 files changed

+107
-63
lines changed

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ BITCOIN_CORE_H = \
231231
util/bip32.h \
232232
util/bytevectorhash.h \
233233
util/check.h \
234+
util/epochguard.h \
234235
util/error.h \
235236
util/fees.h \
236237
util/getuniquepath.h \

src/txmempool.cpp

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFe
2323
int64_t _nTime, unsigned int _entryHeight,
2424
bool _spendsCoinbase, int64_t _sigOpsCost, LockPoints lp)
2525
: tx(_tx), nFee(_nFee), nTxWeight(GetTransactionWeight(*tx)), nUsageSize(RecursiveDynamicUsage(tx)), nTime(_nTime), entryHeight(_entryHeight),
26-
spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp), m_epoch(0)
26+
spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp)
2727
{
2828
nCountWithDescendants = 1;
2929
nSizeWithDescendants = GetTxSize();
@@ -132,7 +132,7 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
132132
// include them, and update their CTxMemPoolEntry::m_parents to include this tx.
133133
// we cache the in-mempool children to avoid duplicate updates
134134
{
135-
const auto epoch = GetFreshEpoch();
135+
WITH_FRESH_EPOCH(m_epoch);
136136
for (; iter != mapNextTx.end() && iter->first->hash == hash; ++iter) {
137137
const uint256 &childHash = iter->second->GetHash();
138138
txiter childIter = mapTx.find(childHash);
@@ -1119,22 +1119,3 @@ void CTxMemPool::SetIsLoaded(bool loaded)
11191119
LOCK(cs);
11201120
m_is_loaded = loaded;
11211121
}
1122-
1123-
1124-
CTxMemPool::EpochGuard CTxMemPool::GetFreshEpoch() const
1125-
{
1126-
return EpochGuard(*this);
1127-
}
1128-
CTxMemPool::EpochGuard::EpochGuard(const CTxMemPool& in) : pool(in)
1129-
{
1130-
assert(!pool.m_has_epoch_guard);
1131-
++pool.m_epoch;
1132-
pool.m_has_epoch_guard = true;
1133-
}
1134-
1135-
CTxMemPool::EpochGuard::~EpochGuard()
1136-
{
1137-
// prevents stale results being used
1138-
++pool.m_epoch;
1139-
pool.m_has_epoch_guard = false;
1140-
}

src/txmempool.h

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <primitives/transaction.h>
2222
#include <random.h>
2323
#include <sync.h>
24+
#include <util/epochguard.h>
2425
#include <util/hasher.h>
2526

2627
#include <boost/multi_index_container.hpp>
@@ -64,6 +65,7 @@ struct CompareIteratorByHash {
6465
return a->GetTx().GetHash() < b->GetTx().GetHash();
6566
}
6667
};
68+
6769
/** \class CTxMemPoolEntry
6870
*
6971
* CTxMemPoolEntry stores data about the corresponding transaction, as well
@@ -156,7 +158,7 @@ class CTxMemPoolEntry
156158
Children& GetMemPoolChildren() const { return m_children; }
157159

158160
mutable size_t vTxHashesIdx; //!< Index in mempool's vTxHashes
159-
mutable uint64_t m_epoch; //!< epoch when last touched, useful for graph algorithms
161+
mutable Epoch::Marker m_epoch_marker; //!< epoch when last touched, useful for graph algorithms
160162
};
161163

162164
// Helpers for modifying CTxMemPool::mapTx, which is a boost multi_index.
@@ -486,8 +488,7 @@ class CTxMemPool
486488
mutable int64_t lastRollingFeeUpdate;
487489
mutable bool blockSinceLastRollingFeeBump;
488490
mutable double rollingMinimumFeeRate; //!< minimum fee to get into the pool, decreases exponentially
489-
mutable uint64_t m_epoch{0};
490-
mutable bool m_has_epoch_guard{false};
491+
mutable Epoch m_epoch GUARDED_BY(cs);
491492

492493
// In-memory counter for external mempool tracking purposes.
493494
// This number is incremented once every time a transaction
@@ -666,7 +667,7 @@ class CTxMemPool
666667
* for). Note: vHashesToUpdate should be the set of transactions from the
667668
* disconnected block that have been accepted back into the mempool.
668669
*/
669-
void UpdateTransactionsFromBlock(const std::vector<uint256>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main);
670+
void UpdateTransactionsFromBlock(const std::vector<uint256>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main) LOCKS_EXCLUDED(m_epoch);
670671

671672
/** Try to calculate all in-mempool ancestors of entry.
672673
* (these are all calculated including the tx itself)
@@ -827,52 +828,22 @@ class CTxMemPool
827828
*/
828829
void removeUnchecked(txiter entry, MemPoolRemovalReason reason) EXCLUSIVE_LOCKS_REQUIRED(cs);
829830
public:
830-
/** EpochGuard: RAII-style guard for using epoch-based graph traversal algorithms.
831-
* When walking ancestors or descendants, we generally want to avoid
832-
* visiting the same transactions twice. Some traversal algorithms use
833-
* std::set (or setEntries) to deduplicate the transaction we visit.
834-
* However, use of std::set is algorithmically undesirable because it both
835-
* adds an asymptotic factor of O(log n) to traverals cost and triggers O(n)
836-
* more dynamic memory allocations.
837-
* In many algorithms we can replace std::set with an internal mempool
838-
* counter to track the time (or, "epoch") that we began a traversal, and
839-
* check + update a per-transaction epoch for each transaction we look at to
840-
* determine if that transaction has not yet been visited during the current
841-
* traversal's epoch.
842-
* Algorithms using std::set can be replaced on a one by one basis.
843-
* Both techniques are not fundamentally incompatible across the codebase.
844-
* Generally speaking, however, the remaining use of std::set for mempool
845-
* traversal should be viewed as a TODO for replacement with an epoch based
846-
* traversal, rather than a preference for std::set over epochs in that
847-
* algorithm.
848-
*/
849-
class EpochGuard {
850-
const CTxMemPool& pool;
851-
public:
852-
explicit EpochGuard(const CTxMemPool& in);
853-
~EpochGuard();
854-
};
855-
// N.B. GetFreshEpoch modifies mutable state via the EpochGuard construction
856-
// (and later destruction)
857-
EpochGuard GetFreshEpoch() const EXCLUSIVE_LOCKS_REQUIRED(cs);
858-
859831
/** visited marks a CTxMemPoolEntry as having been traversed
860-
* during the lifetime of the most recently created EpochGuard
832+
* during the lifetime of the most recently created Epoch::Guard
861833
* and returns false if we are the first visitor, true otherwise.
862834
*
863-
* An EpochGuard must be held when visited is called or an assert will be
835+
* An Epoch::Guard must be held when visited is called or an assert will be
864836
* triggered.
865837
*
866838
*/
867-
bool visited(txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
868-
assert(m_has_epoch_guard);
869-
bool ret = it->m_epoch >= m_epoch;
870-
it->m_epoch = std::max(it->m_epoch, m_epoch);
871-
return ret;
839+
bool visited(const txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch)
840+
{
841+
return m_epoch.visited(it->m_epoch_marker);
872842
}
873843

874-
bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
875-
assert(m_has_epoch_guard);
844+
bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch)
845+
{
846+
assert(m_epoch.guarded()); // verify guard even when it==nullopt
876847
return !it || visited(*it);
877848
}
878849
};

src/util/epochguard.h

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2009-2010 Satoshi Nakamoto
2+
// Copyright (c) 2009-2020 The Bitcoin Core developers
3+
// Distributed under the MIT software license, see the accompanying
4+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
#ifndef BITCOIN_UTIL_EPOCHGUARD_H
7+
#define BITCOIN_UTIL_EPOCHGUARD_H
8+
9+
#include <threadsafety.h>
10+
11+
#include <cassert>
12+
13+
/** Epoch: RAII-style guard for using epoch-based graph traversal algorithms.
14+
* When walking ancestors or descendants, we generally want to avoid
15+
* visiting the same transactions twice. Some traversal algorithms use
16+
* std::set (or setEntries) to deduplicate the transaction we visit.
17+
* However, use of std::set is algorithmically undesirable because it both
18+
* adds an asymptotic factor of O(log n) to traversals cost and triggers O(n)
19+
* more dynamic memory allocations.
20+
* In many algorithms we can replace std::set with an internal mempool
21+
* counter to track the time (or, "epoch") that we began a traversal, and
22+
* check + update a per-transaction epoch for each transaction we look at to
23+
* determine if that transaction has not yet been visited during the current
24+
* traversal's epoch.
25+
* Algorithms using std::set can be replaced on a one by one basis.
26+
* Both techniques are not fundamentally incompatible across the codebase.
27+
* Generally speaking, however, the remaining use of std::set for mempool
28+
* traversal should be viewed as a TODO for replacement with an epoch based
29+
* traversal, rather than a preference for std::set over epochs in that
30+
* algorithm.
31+
*/
32+
33+
class LOCKABLE Epoch
34+
{
35+
private:
36+
uint64_t m_raw_epoch = 0;
37+
bool m_guarded = false;
38+
39+
public:
40+
Epoch() = default;
41+
Epoch(const Epoch&) = delete;
42+
Epoch& operator=(const Epoch&) = delete;
43+
44+
bool guarded() const { return m_guarded; }
45+
46+
class Marker
47+
{
48+
private:
49+
uint64_t m_marker = 0;
50+
51+
// only allow modification via Epoch member functions
52+
friend class Epoch;
53+
Marker& operator=(const Marker&) = delete;
54+
};
55+
56+
class SCOPED_LOCKABLE Guard
57+
{
58+
private:
59+
Epoch& m_epoch;
60+
61+
public:
62+
explicit Guard(Epoch& epoch) EXCLUSIVE_LOCK_FUNCTION(epoch) : m_epoch(epoch)
63+
{
64+
assert(!m_epoch.m_guarded);
65+
++m_epoch.m_raw_epoch;
66+
m_epoch.m_guarded = true;
67+
}
68+
~Guard() UNLOCK_FUNCTION()
69+
{
70+
assert(m_epoch.m_guarded);
71+
++m_epoch.m_raw_epoch; // ensure clear separation between epochs
72+
m_epoch.m_guarded = false;
73+
}
74+
};
75+
76+
bool visited(Marker& marker) const EXCLUSIVE_LOCKS_REQUIRED(*this)
77+
{
78+
assert(m_guarded);
79+
if (marker.m_marker < m_raw_epoch) {
80+
// marker is from a previous epoch, so this is its first visit
81+
marker.m_marker = m_raw_epoch;
82+
return false;
83+
} else {
84+
return true;
85+
}
86+
}
87+
};
88+
89+
#define WITH_FRESH_EPOCH(epoch) const Epoch::Guard PASTE2(epoch_guard_, __COUNTER__)(epoch)
90+
91+
#endif // BITCOIN_UTIL_EPOCHGUARD_H

0 commit comments

Comments
 (0)