Files
rippled/include/xrpl/basics/partitioned_unordered_map.h
Pratik Mankawde 614c1a39ad fix(nodestore): bound the rotation freshen's cache lock hold and measure its yield
The online-delete rotation's cache freshen called TaggedCache::getKeys(),
which held the cache mutex while copying every key. On the dev box's 26
million entry tree-node cache that hold lasted 5-6 s, froze every job
that touches the cache, and dropped the RocksDB node out of sync once per
rotation: each "getKeys held the lock" warning was followed within 1-5 s
by "View of consensus changed" (5 of 5 rotations on 2026-09-15).

Copy the keys one map partition at a time instead. TaggedCache gains
forEachKeyPartition(), which holds the mutex only while one partition's
keys are copied and runs the callback with the mutex released, so the
longest hold shrinks by the partition count (8 on the dev box). The
freshen.keys rotation phase no longer exists as one step, so its span,
stage value, harness entries and docs are removed; the per-partition hold
still shows on the cache lock-hold peak gauge.

Measure what the freshen achieves, which no existing signal did.
DatabaseRotating gains duplicateCopyForwardTotal(), counting archive
copies made on duplicate fetches (the rotation's own copy walk and
freshen); copyForwardTotal() deliberately excludes those. The freshen
phase records rotation_freshen_keys_total{cache,outcome} and stamps
key_count, cache and keys_copied on its span; the copy phase stamps
nodes_copied. A warn log line per freshen reports the same numbers, and
the ledger-sync-health dashboard gets a Rotation Freshen Yield panel.

Log the "STATE->" operating-mode change at warn instead of info. It is
the only record of a mode change with an exact timestamp; the
state_changes_total counter is scraped once a minute and cannot order a
flap against a multi-second event.

Tests: five GTests for forEachKeyPartition (every key once, empty cache,
mutex free during the callback, concurrent insert, lock-hold peak), three
for duplicateCopyForwardTotal over two memory backends, one for the new
counter's series, and the new name literals.
2026-09-15 13:49:26 +01:00

390 lines
7.8 KiB
C++

#pragma once
#include <xrpl/beast/hash/uhash.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <cstddef>
#include <functional>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
namespace xrpl {
template <typename Key>
static std::size_t
extract(Key const& key)
{
return key;
}
template <>
inline std::size_t
extract(std::string const& key)
{
return ::beast::Uhash<>{}(key);
}
template <
typename Key,
typename Value,
typename Hash,
typename Pred = std::equal_to<Key>,
typename Alloc = std::allocator<std::pair<Key const, Value>>>
class PartitionedUnorderedMap
{
std::size_t partitions_;
public:
using key_type = Key;
using mapped_type = Value;
using value_type = std::pair<Key const, mapped_type>;
using size_type = std::size_t;
using difference_type = std::size_t;
using hasher = Hash;
using key_equal = Pred;
using allocator_type = Alloc;
using reference = value_type&;
using const_reference = value_type const&;
using pointer = value_type*;
using const_pointer = value_type const*;
using map_type = std::unordered_map<key_type, mapped_type, hasher, key_equal, allocator_type>;
using partition_map_type = std::vector<map_type>;
struct Iterator
{
using iterator_category = std::forward_iterator_tag;
partition_map_type* map{nullptr};
partition_map_type::iterator ait{};
map_type::iterator mit;
Iterator() = default;
Iterator(partition_map_type* m) : map(m)
{
}
reference
operator*() const
{
return *mit;
}
pointer
operator->() const
{
return &(*mit);
}
void
inc()
{
++mit;
while (mit == ait->end())
{
++ait;
if (ait == map->end())
return;
mit = ait->begin();
}
}
// ++it
Iterator&
operator++()
{
inc();
return *this;
}
// it++
Iterator
operator++(int)
{
Iterator tmp(*this);
inc();
return tmp;
}
friend bool
operator==(Iterator const& lhs, Iterator const& rhs)
{
return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit;
}
};
struct ConstIterator
{
using iterator_category = std::forward_iterator_tag;
partition_map_type* map{nullptr};
partition_map_type::iterator ait{};
map_type::iterator mit;
ConstIterator() = default;
ConstIterator(partition_map_type* m) : map(m)
{
}
ConstIterator(Iterator const& orig) : map(orig.map), ait(orig.ait), mit(orig.mit)
{
}
const_reference
operator*() const
{
return *mit;
}
const_pointer
operator->() const
{
return &(*mit);
}
void
inc()
{
++mit;
while (mit == ait->end())
{
++ait;
if (ait == map->end())
return;
mit = ait->begin();
}
}
// ++it
ConstIterator&
operator++()
{
inc();
return *this;
}
// it++
ConstIterator
operator++(int)
{
ConstIterator tmp(*this);
inc();
return tmp;
}
friend bool
operator==(ConstIterator const& lhs, ConstIterator const& rhs)
{
return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit;
}
};
private:
std::size_t
partitioner(Key const& key) const
{
return extract(key) % partitions_;
}
template <class T>
static void
end(T& it)
{
it.ait = it.map->end();
it.mit = it.map->back().end();
}
template <class T>
static void
begin(T& it)
{
for (it.ait = it.map->begin(); it.ait != it.map->end(); ++it.ait)
{
if (it.ait->begin() == it.ait->end())
continue;
it.mit = it.ait->begin();
return;
}
end(it);
}
public:
PartitionedUnorderedMap(std::optional<std::size_t> partitions = std::nullopt)
// Set partitions to the number of hardware threads if the parameter
// is either empty or set to 0.
: partitions_(
partitions && (*partitions != 0u) ? *partitions : std::thread::hardware_concurrency())
{
map_.resize(partitions_);
XRPL_ASSERT(
partitions_,
"xrpl::PartitionedUnorderedMap::PartitionedUnorderedMap : "
"nonzero partitions");
}
std::size_t
partitions() const
{
return partitions_;
}
partition_map_type&
map()
{
return map_;
}
partition_map_type const&
map() const
{
return map_;
}
Iterator
begin()
{
Iterator it(&map_);
begin(it);
return it;
}
ConstIterator
cbegin() const
{
ConstIterator it(&map_);
begin(it);
return it;
}
ConstIterator
begin() const
{
return cbegin();
}
Iterator
end()
{
Iterator it(&map_);
end(it);
return it;
}
ConstIterator
cend() const
{
ConstIterator it(&map_);
end(it);
return it;
}
ConstIterator
end() const
{
return cend();
}
private:
template <class T>
void
find(key_type const& key, T& it) const
{
it.ait = it.map->begin() + partitioner(key);
it.mit = it.ait->find(key);
if (it.mit == it.ait->end())
end(it);
}
public:
Iterator
find(key_type const& key)
{
Iterator it(&map_);
find(key, it);
return it;
}
ConstIterator
find(key_type const& key) const
{
ConstIterator it(&map_);
find(key, it);
return it;
}
template <class T, class U>
std::pair<Iterator, bool>
emplace(std::piecewise_construct_t const&, T&& keyTuple, U&& valueTuple)
{
auto const& key = std::get<0>(keyTuple);
Iterator it(&map_);
it.ait = it.map->begin() + partitioner(key);
auto [eit, inserted] = it.ait->emplace(
std::piecewise_construct, std::forward<T>(keyTuple), std::forward<U>(valueTuple));
it.mit = eit;
return {it, inserted};
}
template <class T, class U>
std::pair<Iterator, bool>
emplace(T&& key, U&& val)
{
Iterator it(&map_);
it.ait = it.map->begin() + partitioner(key);
auto [eit, inserted] = it.ait->emplace(std::forward<T>(key), std::forward<U>(val));
it.mit = eit;
return {it, inserted};
}
void
clear()
{
for (auto& p : map_)
p.clear();
}
Iterator
erase(ConstIterator position)
{
Iterator it(&map_);
it.ait = position.ait;
it.mit = position.ait->erase(position.mit);
while (it.mit == it.ait->end())
{
++it.ait;
if (it.ait == it.map->end())
break;
it.mit = it.ait->begin();
}
return it;
}
std::size_t
size() const
{
std::size_t ret = 0;
for (auto& p : map_)
ret += p.size();
return ret;
}
Value&
operator[](Key const& key)
{
return map_[partitioner(key)][key];
}
private:
mutable partition_map_type map_{};
};
} // namespace xrpl