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.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
No conflicts as of last run.
75+private:
76+ uint64_t m_raw_epoch;
77+ bool m_guarded;
78+
79+public:
80+ Epoch() : m_raw_epoch{0}, m_guarded{false} { }
87+ class Marker {
88+ private:
89+ uint64_t m_marker;
90+
91+ public:
92+ Marker() : m_marker{0} { }
78+
79+public:
80+ Epoch() : m_raw_epoch{0}, m_guarded{false} { }
81+
82+ Epoch(const Epoch&) = delete; // no copy constructor
83+ Epoch& operator=(const Epoch&) = delete; // not assignable
68+ * traversal should be viewed as a TODO for replacement with an epoch based
69+ * traversal, rather than a preference for std::set over epochs in that
70+ * algorithm.
71+ */
72+
73+class Epoch
Tested on top of the #18635, and had loads of warnings:
0./txmempool.h:102:38: warning: 'exclusive_lock_function' attribute requires arguments whose type is annotated with 'capability' attribute; type here is 'Epoch' [-Wthread-safety-attributes]
1 explicit Guard(Epoch& epoch) EXCLUSIVE_LOCK_FUNCTION(epoch) LOCKS_EXCLUDED(epoch);
2 ^
3...
4./txmempool.h:106:40: warning: 'exclusive_locks_required' attribute requires arguments whose type is annotated with 'capability' attribute; type here is 'const Epoch' [-Wthread-safety-attributes]
5 bool visited(Marker& marker) const EXCLUSIVE_LOCKS_REQUIRED(*this) {
6 ^
7...
8./txmempool.h:691:121: warning: 'locks_excluded' attribute requires arguments whose type is annotated with 'capability' attribute; type here is 'Epoch' [-Wthread-safety-attributes]
9 void UpdateTransactionsFromBlock(const std::vector<uint256>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main) LOCKS_EXCLUDED(m_epoch);
10 ^
11...
12./txmempool.h:812:41: warning: 'exclusive_locks_required' attribute requires arguments whose type is annotated with 'capability' attribute; type here is 'Epoch' [-Wthread-safety-attributes]
13 bool visited(const txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
14 ^
15...
16./txmempool.h:816:45: warning: 'exclusive_locks_required' attribute requires arguments whose type is annotated with 'capability' attribute; type here is 'Epoch' [-Wthread-safety-attributes]
17 bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
18 ^
To make the Clang Thread Safety Analysis work as expected I suggest:
0class LOCKABLE Epoch
and change the type of the Epoch::Guard::m_epoch
from Epoch&
to Epoch*
.
Epoch::Guard::m_epoch
from reference to pointer would have – the analysis shouldn’t (and doesn’t) treat them any different, as far as I can see?
97+ class SCOPED_LOCKABLE Guard {
98+ private:
99+ Epoch& m_epoch;
100+
101+ public:
102+ explicit Guard(Epoch& epoch) EXCLUSIVE_LOCK_FUNCTION(epoch) LOCKS_EXCLUDED(epoch);
ACK 837d4e4f5aeb2f110143c59b7dd05eac6ae52b63, tested on Linux Mint 19.3: the Clang’s thread safety annotations indeed reduce chances of the Epoch
class misuse (verified by different code manipulations).
May I suggest two additional annotations:
0--- a/src/txmempool.cpp
1+++ b/src/txmempool.cpp
2@@ -107,6 +107,7 @@ void CTxMemPool::UpdateForDescendants(txiter updateIt, cacheMap &cachedDescendan
3 void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashesToUpdate)
4 {
5 AssertLockHeld(cs);
6+ AssertLockNotHeld(m_epoch);
7 // For each entry in vHashesToUpdate, store the set of in-mempool, but not
8 // in-vHashesToUpdate transactions, so that we don't have to recalculate
9 // descendants when we come across a previously seen entry.
10diff --git a/src/txmempool.h b/src/txmempool.h
11index 655afc3b8..b72ad229c 100644
12--- a/src/txmempool.h
13+++ b/src/txmempool.h
14@@ -797,7 +797,7 @@ private:
15 */
16 void UpdateForDescendants(txiter updateIt,
17 cacheMap &cachedDescendants,
18- const std::set<uint256> &setExclude) EXCLUSIVE_LOCKS_REQUIRED(cs);
19+ const std::set<uint256> &setExclude) EXCLUSIVE_LOCKS_REQUIRED(cs) LOCKS_EXCLUDED(m_epoch);
20 /** Update ancestors of hash to add/remove it as a descendant transaction. */
21 void UpdateAncestorsOf(bool add, txiter hash, setEntries &setAncestors) EXCLUSIVE_LOCKS_REQUIRED(cs);
22 /** Set ancestor state for an entry */
?
93+public:
94+ Epoch() { }
95+ Epoch(const Epoch&) = delete;
96+ Epoch& operator=(const Epoch&) = delete;
97+
98+ bool guarded() const { return m_guarded; }
assert(m_epoch.guarded());
in CTxMemPool::visited(Optional<txiter> it)
to check the lock’s held in the case where it->visited
isn’t called.
115+ bool visited(Marker& marker) const EXCLUSIVE_LOCKS_REQUIRED(*this) {
116+ assert(m_guarded);
117+ bool ret = marker.m_marker >= m_raw_epoch;
118+ if (!ret) marker.m_marker = m_raw_epoch;
119+ return ret;
120+ }
nit: how would you feel about:
0uint64_t marker_old = marker.m_marker;
1marker.m_marker = std::max(marker.m_marker, m_raw_epoch);
2return marker.m_marker != marker_old;
for whatever reason I find it easier to read/reason about when we always set it to max.
That reverses the logic (ret == false
causes m_marker
to change originally, but your code returns true
if m_marker
changed) ? Seems like that’s evidence it’s not that easy to read/reason about? :)
It might be less confusing to write it out in full:
0if (marker.m_marker < m_raw_epoch ) {
1 // this entry is from an earlier epoch, so it hasn't been visited
2 marker.m_marker = m_raw_epoch;
3 return false;
4} else {
5 return true;
6}
I’d guess the compiler will make all these variants essentially the same anyway, so no objection to any variation from me.
Ah my bad, I’ve made a case against myself – for what it’s worth, I made the proposed version by translating your proposed version, which I have trouble parsing logically, and thought it was equivalent to that logic (and still can’t figure out the concrete reason it’s opposite).
I’m fine with the completely written out version as it is the most obvious for sleepy brains like mine.
I think the translation goes:
r = a >= b; if (!r) a = b; return r;
(current)o = a; if (! (a >= b)) a = b; return o >= b;
(store old value, replace r
)o = a; if (a < b) a = b; return !(o < b);
(switch to less than)o = a; a = max(a,b); return !(o != a);
(o < b
iff the if was taken, and a
was changed)o = a; a = max(a,b); return o == a;
(simplify)Anyway, changed to KISS version.
1112@@ -1113,22 +1113,17 @@ void CTxMemPool::SetIsLoaded(bool loaded)
1113 }
1114
1115
1116-CTxMemPool::EpochGuard CTxMemPool::GetFreshEpoch() const
1117+Epoch::Guard::Guard(Epoch& epoch) : m_epoch(epoch)
864 }
865
866- bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
867- assert(m_has_epoch_guard);
868+ bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
869+ assert(m_epoch.guarded());
20@@ -21,6 +21,7 @@
21 #include <primitives/transaction.h>
22 #include <sync.h>
23 #include <random.h>
24+#include <util/epochguard.h>
style nit: while touching headers they could be sorted:
0#include <random.h>
1#include <sync.h>
2#include <util/epochguard.h>
862- bool visited(txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
863- assert(m_has_epoch_guard);
864- bool ret = it->m_epoch >= m_epoch;
865- it->m_epoch = std::max(it->m_epoch, m_epoch);
866- return ret;
867+ bool visited(const txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
style nit, suggested by clang-format
:
0 bool visited(const txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch)
1 {
868+ return m_epoch.visited(it->m_epoch_marker);
869 }
870
871- bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
872- assert(m_has_epoch_guard);
873+ bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
style nit, suggested by clang-format
:
0 bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch)
1 {
35+private:
36+ uint64_t m_raw_epoch = 0;
37+ bool m_guarded = false;
38+
39+public:
40+ Epoch() { }
style nit, suggested by clang-format
:
0 Epoch() {}
41+ Epoch(const Epoch&) = delete;
42+ Epoch& operator=(const Epoch&) = delete;
43+
44+ bool guarded() const { return m_guarded; }
45+
46+ class Marker {
style nit, suggested by clang-format
:
0 class Marker
1 {
50+ // only allow modification via Epoch member functions
51+ friend class Epoch;
52+ Marker& operator=(const Marker&) = delete;
53+ };
54+
55+ class SCOPED_LOCKABLE Guard {
style nit, suggested by clang-format
:
0 class SCOPED_LOCKABLE Guard
1 {
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 traverals cost and triggers O(n)
typo, #17925 (review):
0 * adds an asymptotic factor of O(log n) to traversals cost and triggers O(n)
63+ ++m_epoch.m_raw_epoch;
64+ m_epoch.m_guarded = true;
65+ }
66+ ~Guard() UNLOCK_FUNCTION()
67+ {
68+ ++m_epoch.m_raw_epoch; // ensure clear separation between epochs
0 {
1 assert(m_epoch.m_guarded);
2 ++m_epoch.m_raw_epoch; // ensure clear separation between epochs
ACK 88019f9a183d396713fd357604a6e472c5ed8807
It seems both CTxMemPool::visited
member functions could be private
.
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
0 * counter to track the time (or, "epoch") that we began a traversal and
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
0 * traversal should be viewed as a TODO for replacement with an epoch-based
ACK fd6580e405699ccb051fd2a34525e48d3253673d 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
example clang warning
0/txmempool.h:840:24: warning: calling function 'visited' requires holding mutex 'm_epoch' exclusively [-Wthread-safety-analysis]
1 return m_epoch.visited(it->m_epoch_marker);
Ignore the two comments below unless you need to retouch anything (the documentation was move-only anyway).