Skip to content

Commit

Permalink
Merge bitcoin#18017: txmempool: split epoch logic into class
Browse files Browse the repository at this point in the history
fd6580e [refactor] txmempool: split epoch logic into class (Anthony Towns)

Pull request description:

  Splits the epoch logic introduced in bitcoin#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#18017 (review)) review:

Tree-SHA512: 7004623faa02b56639aa05ab7a078320a6d8d54ec62d8022876221e33f350f47df51ddff056c0de5be798f8eb39b5c03c2d3f035698555d70abc218e950f2f8c
  • Loading branch information
laanwj authored and UdjinM6 committed Dec 8, 2023
1 parent b8267bb commit 99c1307
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 62 deletions.
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ BITCOIN_CORE_H = \
util/bytevectorhash.h \
util/check.h \
util/enumerate.h \
util/epochguard.h \
util/error.h \
util/fastrange.h \
util/fees.h \
Expand Down
22 changes: 2 additions & 20 deletions src/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFe
int64_t _nTime, unsigned int _entryHeight,
bool _spendsCoinbase, unsigned int _sigOps, LockPoints lp)
: tx(_tx), nFee(_nFee), nTxSize(tx->GetTotalSize()), nUsageSize(RecursiveDynamicUsage(tx)), nTime(_nTime), entryHeight(_entryHeight),
spendsCoinbase(_spendsCoinbase), sigOpCount(_sigOps), lockPoints(lp), m_epoch(0)
spendsCoinbase(_spendsCoinbase), sigOpCount(_sigOps), lockPoints(lp)
{
nCountWithDescendants = 1;
nSizeWithDescendants = GetTxSize();
Expand Down Expand Up @@ -141,7 +141,7 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
// include them, and update their setMemPoolParents to include this tx.
// we cache the in-mempool children to avoid duplicate updates
{
const auto epoch = GetFreshEpoch();
WITH_FRESH_EPOCH(m_epoch);
for (; iter != mapNextTx.end() && iter->first->hash == hash; ++iter) {
const uint256 &childHash = iter->second->GetHash();
txiter childIter = mapTx.find(childHash);
Expand Down Expand Up @@ -1628,24 +1628,6 @@ void CTxMemPool::GetTransactionAncestry(const uint256& txid, size_t& ancestors,
}
}

CTxMemPool::EpochGuard CTxMemPool::GetFreshEpoch() const
{
return EpochGuard(*this);
}
CTxMemPool::EpochGuard::EpochGuard(const CTxMemPool& in) : pool(in)
{
assert(!pool.m_has_epoch_guard);
++pool.m_epoch;
pool.m_has_epoch_guard = true;
}

CTxMemPool::EpochGuard::~EpochGuard()
{
// prevents stale results being used
++pool.m_epoch;
pool.m_has_epoch_guard = false;
}

bool CTxMemPool::IsLoaded() const
{
LOCK(cs);
Expand Down
53 changes: 11 additions & 42 deletions src/txmempool.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <random.h>
#include <netaddress.h>
#include <pubkey.h>
#include <util/epochguard.h>
#include <util/hasher.h>

#include <boost/optional.hpp>
Expand Down Expand Up @@ -142,7 +143,7 @@ class CTxMemPoolEntry
// If this is a proTx, this will be the hash of the key for which this ProTx was valid
mutable uint256 validForProTxKey;
mutable bool isKeyChangeProTx{false};
mutable uint64_t m_epoch; //!< epoch when last touched, useful for graph algorithms
mutable Epoch::Marker m_epoch_marker; //!< epoch when last touched, useful for graph algorithms
};

// Helpers for modifying CTxMemPool::mapTx, which is a boost multi_index.
Expand Down Expand Up @@ -452,8 +453,7 @@ class CTxMemPool
mutable int64_t lastRollingFeeUpdate;
mutable bool blockSinceLastRollingFeeBump;
mutable double rollingMinimumFeeRate; //!< minimum fee to get into the pool, decreases exponentially
mutable uint64_t m_epoch{0};
mutable bool m_has_epoch_guard{false};
mutable Epoch m_epoch GUARDED_BY(cs);

void trackPackageRemoved(const CFeeRate& rate) EXCLUSIVE_LOCKS_REQUIRED(cs);

Expand Down Expand Up @@ -668,7 +668,7 @@ class CTxMemPool
* for). Note: vHashesToUpdate should be the set of transactions from the
* disconnected block that have been accepted back into the mempool.
*/
void UpdateTransactionsFromBlock(const std::vector<uint256>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main);
void UpdateTransactionsFromBlock(const std::vector<uint256>& vHashesToUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs, cs_main) LOCKS_EXCLUDED(m_epoch);

/** Try to calculate all in-mempool ancestors of entry.
* (these are all calculated including the tx itself)
Expand Down Expand Up @@ -804,52 +804,21 @@ class CTxMemPool
*/
void removeUnchecked(txiter entry, MemPoolRemovalReason reason) EXCLUSIVE_LOCKS_REQUIRED(cs);
public:
/** EpochGuard: RAII-style guard for using epoch-based graph traversal algorithms.
* When walking ancestors or descendants, we generally want to avoid
* visiting the same transactions twice. Some traversal algorithms use
* std::set (or setEntries) to deduplicate the transaction we visit.
* However, use of std::set is algorithmically undesirable because it both
* adds an asymptotic factor of O(log n) to traverals cost and triggers O(n)
* more dynamic memory allocations.
* In many algorithms we can replace std::set with an internal mempool
* counter to track the time (or, "epoch") that we began a traversal, and
* check + update a per-transaction epoch for each transaction we look at to
* determine if that transaction has not yet been visited during the current
* traversal's epoch.
* Algorithms using std::set can be replaced on a one by one basis.
* Both techniques are not fundamentally incompatible across the codebase.
* Generally speaking, however, the remaining use of std::set for mempool
* traversal should be viewed as a TODO for replacement with an epoch based
* traversal, rather than a preference for std::set over epochs in that
* algorithm.
*/
class EpochGuard {
const CTxMemPool& pool;
public:
explicit EpochGuard(const CTxMemPool& in);
~EpochGuard();
};
// N.B. GetFreshEpoch modifies mutable state via the EpochGuard construction
// (and later destruction)
EpochGuard GetFreshEpoch() const EXCLUSIVE_LOCKS_REQUIRED(cs);

/** visited marks a CTxMemPoolEntry as having been traversed
* during the lifetime of the most recently created EpochGuard
* during the lifetime of the most recently created Epoch::Guard
* and returns false if we are the first visitor, true otherwise.
*
* An EpochGuard must be held when visited is called or an assert will be
* An Epoch::Guard must be held when visited is called or an assert will be
* triggered.
*
*/
bool visited(txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
assert(m_has_epoch_guard);
bool ret = it->m_epoch >= m_epoch;
it->m_epoch = std::max(it->m_epoch, m_epoch);
return ret;
bool visited(const txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch)
{
return m_epoch.visited(it->m_epoch_marker);
}

bool visited(boost::optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
assert(m_has_epoch_guard);
bool visited(boost::optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs, m_epoch) {
assert(m_epoch.guarded());
return !it || visited(*it);
}
};
Expand Down
91 changes: 91 additions & 0 deletions src/util/epochguard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_UTIL_EPOCHGUARD_H
#define BITCOIN_UTIL_EPOCHGUARD_H

#include <threadsafety.h>

#include <cassert>

/** Epoch: RAII-style guard for using epoch-based graph traversal algorithms.
* When walking ancestors or descendants, we generally want to avoid
* visiting the same transactions twice. Some traversal algorithms use
* std::set (or setEntries) to deduplicate the transaction we visit.
* However, use of std::set is algorithmically undesirable because it both
* adds an asymptotic factor of O(log n) to traversals cost and triggers O(n)
* more dynamic memory allocations.
* In many algorithms we can replace std::set with an internal mempool
* counter to track the time (or, "epoch") that we began a traversal, and
* check + update a per-transaction epoch for each transaction we look at to
* determine if that transaction has not yet been visited during the current
* traversal's epoch.
* Algorithms using std::set can be replaced on a one by one basis.
* Both techniques are not fundamentally incompatible across the codebase.
* Generally speaking, however, the remaining use of std::set for mempool
* traversal should be viewed as a TODO for replacement with an epoch based
* traversal, rather than a preference for std::set over epochs in that
* algorithm.
*/

class LOCKABLE Epoch
{
private:
uint64_t m_raw_epoch = 0;
bool m_guarded = false;

public:
Epoch() = default;
Epoch(const Epoch&) = delete;
Epoch& operator=(const Epoch&) = delete;

bool guarded() const { return m_guarded; }

class Marker
{
private:
uint64_t m_marker = 0;

// only allow modification via Epoch member functions
friend class Epoch;
Marker& operator=(const Marker&) = delete;
};

class SCOPED_LOCKABLE Guard
{
private:
Epoch& m_epoch;

public:
explicit Guard(Epoch& epoch) EXCLUSIVE_LOCK_FUNCTION(epoch) : m_epoch(epoch)
{
assert(!m_epoch.m_guarded);
++m_epoch.m_raw_epoch;
m_epoch.m_guarded = true;
}
~Guard() UNLOCK_FUNCTION()
{
assert(m_epoch.m_guarded);
++m_epoch.m_raw_epoch; // ensure clear separation between epochs
m_epoch.m_guarded = false;
}
};

bool visited(Marker& marker) const EXCLUSIVE_LOCKS_REQUIRED(*this)
{
assert(m_guarded);
if (marker.m_marker < m_raw_epoch) {
// marker is from a previous epoch, so this is its first visit
marker.m_marker = m_raw_epoch;
return false;
} else {
return true;
}
}
};

#define WITH_FRESH_EPOCH(epoch) const Epoch::Guard PASTE2(epoch_guard_, __COUNTER__)(epoch)

#endif // BITCOIN_UTIL_EPOCHGUARD_H

0 comments on commit 99c1307

Please sign in to comment.