mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-24 15:50:55 +00:00
Compare commits
5 Commits
Wasm-vm-re
...
dangell7/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f80e72c6f7 | ||
|
|
c16f18f79b | ||
|
|
a40b88436d | ||
|
|
c65e4539f5 | ||
|
|
45ed9e4de7 |
@@ -28,6 +28,8 @@ This section contains changes targeting a future version.
|
||||
|
||||
### Additions
|
||||
|
||||
- `server_info` (admin): The `node_size` field has been removed along with the deprecated `[node_size]` config setting it reported. Admin responses now include `memory_limit`, the cache memory budget in gigabytes (0 when enforcement is disabled).
|
||||
|
||||
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
|
||||
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
|
||||
|
||||
|
||||
@@ -1306,23 +1306,40 @@
|
||||
#
|
||||
# [node_size]
|
||||
#
|
||||
# Tunes the servers based on the expected load and available memory. Legal
|
||||
# sizes are "tiny", "small", "medium", "large", and "huge". We recommend
|
||||
# you start at the default and raise the setting if you have extra memory.
|
||||
# DEPRECATED. Each size is now an alias for a [memory_limit] value:
|
||||
# tiny = 4, small = 8, medium = 32, large = 64, huge = 128. Set
|
||||
# [memory_limit] instead; setting this logs a warning at startup.
|
||||
#
|
||||
# The code attempts to automatically determine the appropriate size for
|
||||
# this parameter based on the amount of RAM and the number of execution
|
||||
# cores available to the server. The current decision matrix is:
|
||||
# [memory_limit]
|
||||
#
|
||||
# | | Cores |
|
||||
# |---------|------------------------|
|
||||
# | RAM | 1 | 2 or 3 | ≥ 4 |
|
||||
# |---------|------|--------|--------|
|
||||
# | < ~8GB | tiny | tiny | tiny |
|
||||
# | < ~12GB | tiny | small | small |
|
||||
# | < ~16GB | tiny | small | medium |
|
||||
# | < ~24GB | tiny | small | large |
|
||||
# | < ~32GB | tiny | small | huge |
|
||||
# The memory budget, in gigabytes, that the server sizes its caches
|
||||
# within. Cache sizes scale with the budget; the SHAMap tree node cache
|
||||
# is capped to fit within half of it, enforced as it grows. Defaults to
|
||||
# detected physical RAM (capped by the container limit when one is set);
|
||||
# 0 selects minimal sizes with no enforcement. Values above 1024 are
|
||||
# rejected, and a value above detected RAM logs a warning.
|
||||
# Set this when xrpld shares the machine with other services or runs in
|
||||
# a container with a memory limit below the host's RAM. Thread counts
|
||||
# are unrelated: they come from the core count and the [workers] /
|
||||
# [io_workers] overrides.
|
||||
#
|
||||
# Example:
|
||||
# memory_limit = 16
|
||||
#
|
||||
# [tree_cache_age]
|
||||
#
|
||||
# Seconds a SHAMap tree node stays cached after its last use. The default
|
||||
# is 300. Accepted values are 10 to 3600.
|
||||
#
|
||||
# [ledger_cache_age]
|
||||
#
|
||||
# Seconds a full ledger stays in the ledger cache after its last use. The
|
||||
# default is 180. Accepted values are 10 to 3600.
|
||||
#
|
||||
# [ledger_fetch_size]
|
||||
#
|
||||
# How many historical ledgers to acquire per fetch pass while backfilling.
|
||||
# The default is 4. Accepted values are 1 to 16.
|
||||
#
|
||||
# [signing_support]
|
||||
#
|
||||
|
||||
@@ -74,13 +74,20 @@ public:
|
||||
using shared_pointer_type = SharedPointerType;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param cacheHardCap When positive, a hard upper bound on the number of
|
||||
* strongly-cached entries, enforced by demoting the approximately
|
||||
* oldest entry whenever growth would exceed it. 0 disables the cap
|
||||
* (the periodic sweep alone bounds the cache).
|
||||
*/
|
||||
TaggedCache(
|
||||
std::string const& name,
|
||||
int size,
|
||||
clock_type::duration expiration,
|
||||
clock_type& clock,
|
||||
beast::Journal journal,
|
||||
beast::insight::Collector::ptr const& collector = beast::insight::NullCollector::make());
|
||||
beast::insight::Collector::ptr const& collector = beast::insight::NullCollector::make(),
|
||||
int cacheHardCap = 0);
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -357,6 +364,13 @@ private:
|
||||
|
||||
using cache_type = hardened_partitioned_hash_map<key_type, Entry, Hash, KeyEqual>;
|
||||
|
||||
// Bounded approximate-LRU eviction from a single partition. Keeps the
|
||||
// strong-entry count at/below cacheHardCap_ as new entries are inserted, so
|
||||
// a burst can't drive the cache past its RAM budget between timer sweeps.
|
||||
// No-op unless cacheHardCap_ > 0 (opt-in); caller holds mutex_.
|
||||
void
|
||||
evictForHardCap(cache_type::map_type& partition, cache_type::map_type::iterator const& keep);
|
||||
|
||||
[[nodiscard]] std::thread
|
||||
sweepHelper(
|
||||
clock_type::time_point const& whenExpire,
|
||||
@@ -390,8 +404,23 @@ private:
|
||||
// Desired maximum cache age
|
||||
clock_type::duration const targetAge_;
|
||||
|
||||
// Hard upper bound on strongly-cached entries, enforced by
|
||||
// evictForHardCap whenever the strong count grows (fresh inserts and
|
||||
// weak-to-strong revivals). 0 disables it (sweep-only sizing).
|
||||
int const cacheHardCap_;
|
||||
|
||||
// Total hard-cap evictions (under mutex_); the first marks saturation
|
||||
// onset for logging.
|
||||
std::uint64_t hardCapEvictions_{0};
|
||||
|
||||
// Number of items cached
|
||||
int cacheCount_{0};
|
||||
|
||||
// Rotating bucket cursor for evictForHardCap so successive over-cap
|
||||
// evictions sweep the whole partition (CLOCK hand) instead of repeatedly
|
||||
// sampling the head buckets. Advanced under mutex_.
|
||||
std::size_t evictHand_{0};
|
||||
|
||||
cache_type cache_; // Hold strong reference to recent objects
|
||||
std::uint64_t hits_{0};
|
||||
std::uint64_t misses_{0};
|
||||
|
||||
@@ -57,7 +57,8 @@ inline TaggedCache<
|
||||
clock_type::duration expiration,
|
||||
clock_type& clock,
|
||||
beast::Journal journal,
|
||||
beast::insight::Collector::ptr const& collector)
|
||||
beast::insight::Collector::ptr const& collector,
|
||||
int cacheHardCap)
|
||||
: journal_(journal)
|
||||
, clock_(clock)
|
||||
, stats_(
|
||||
@@ -67,6 +68,7 @@ inline TaggedCache<
|
||||
, name_(name)
|
||||
, targetSize_(size)
|
||||
, targetAge_(expiration)
|
||||
, cacheHardCap_(cacheHardCap)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -219,6 +221,102 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
return true;
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
bool IsKeyCache,
|
||||
class SharedWeakUnionPointer,
|
||||
class SharedPointerType,
|
||||
class Hash,
|
||||
class KeyEqual,
|
||||
class Mutex>
|
||||
inline void
|
||||
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
|
||||
evictForHardCap(cache_type::map_type& partition, cache_type::map_type::iterator const& keep)
|
||||
{
|
||||
// Caller holds mutex_. Only value caches carry strong/weak entries; key
|
||||
// caches never enable the hard cap, so this is a no-op for them.
|
||||
if constexpr (!IsKeyCache)
|
||||
{
|
||||
std::size_t const bucketCount = partition.bucket_count();
|
||||
if (bucketCount == 0)
|
||||
return;
|
||||
|
||||
// Approximate LRU with bounded work per call: sample a window of
|
||||
// strong entries starting at the rotating bucket cursor and demote
|
||||
// the oldest, repeating until the count is back under the cap or the
|
||||
// demotion budget is spent. Growth paths raise the count by one at a
|
||||
// time, so the budget lets eviction catch up without stalling them.
|
||||
constexpr int kEvictSampleBudget = 64;
|
||||
constexpr int kMaxDemotionsPerCall = 8;
|
||||
std::size_t const maxBuckets = std::min<std::size_t>(bucketCount, 4 * kEvictSampleBudget);
|
||||
|
||||
for (int demotions = 0; cacheCount_ > cacheHardCap_ && demotions < kMaxDemotionsPerCall;
|
||||
++demotions)
|
||||
{
|
||||
int sampled = 0;
|
||||
std::size_t bucketsWalked = 0;
|
||||
key_type oldestKey{};
|
||||
bool haveOldest = false;
|
||||
clock_type::time_point oldestAccess{};
|
||||
|
||||
std::size_t b = evictHand_ % bucketCount;
|
||||
while (sampled < kEvictSampleBudget && bucketsWalked < maxBuckets)
|
||||
{
|
||||
for (auto lit = partition.begin(b); lit != partition.end(b); ++lit)
|
||||
{
|
||||
if (lit->first == keep->first || lit->second.isWeak())
|
||||
continue;
|
||||
if (!haveOldest || lit->second.lastAccess < oldestAccess)
|
||||
{
|
||||
oldestAccess = lit->second.lastAccess;
|
||||
oldestKey = lit->first;
|
||||
haveOldest = true;
|
||||
}
|
||||
if (++sampled >= kEvictSampleBudget)
|
||||
break;
|
||||
}
|
||||
b = (b + 1) % bucketCount;
|
||||
++bucketsWalked;
|
||||
}
|
||||
evictHand_ = b; // resume the scan here on the next over-cap call
|
||||
|
||||
if (!haveOldest)
|
||||
{
|
||||
JLOG(journal_.debug()) << name_ << ": over hard cap " << cacheHardCap_
|
||||
<< " but eviction sample found no strong entry to demote";
|
||||
return;
|
||||
}
|
||||
|
||||
auto oldest = partition.find(oldestKey);
|
||||
if (oldest == partition.end() || oldest == keep || oldest->second.isWeak())
|
||||
return;
|
||||
|
||||
if (oldest->second.ptr.useCount() == 1)
|
||||
{
|
||||
// Sole owner: release entirely.
|
||||
partition.erase(oldest);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Others hold it: keep it weakly tracked.
|
||||
oldest->second.ptr.convertToWeak();
|
||||
}
|
||||
--cacheCount_;
|
||||
|
||||
// First eviction marks saturation onset; then a heartbeat every
|
||||
// 100k to avoid flooding.
|
||||
++hardCapEvictions_;
|
||||
if (hardCapEvictions_ == 1 || hardCapEvictions_ % 100000 == 0)
|
||||
{
|
||||
JLOG(journal_.warn()) << name_ << ": hard-cap eviction #" << hardCapEvictions_
|
||||
<< " (cap " << cacheHardCap_ << ", strong " << cacheCount_
|
||||
<< ") - cache saturated, growth now evicts";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <
|
||||
class Key,
|
||||
class T,
|
||||
@@ -360,11 +458,17 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
|
||||
if (cit == cache_.end())
|
||||
{
|
||||
cache_.emplace(
|
||||
std::piecewise_construct,
|
||||
std::forward_as_tuple(key),
|
||||
std::forward_as_tuple(clock_.now(), data));
|
||||
auto const emplacedIt = cache_
|
||||
.emplace(
|
||||
std::piecewise_construct,
|
||||
std::forward_as_tuple(key),
|
||||
std::forward_as_tuple(clock_.now(), data))
|
||||
.first;
|
||||
++cacheCount_;
|
||||
// The just-inserted entry is the newest; evictForHardCap skips it
|
||||
// and drops the oldest in its partition.
|
||||
if (cacheHardCap_ > 0 && cacheCount_ > cacheHardCap_)
|
||||
evictForHardCap(*emplacedIt.ait, emplacedIt.mit);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -415,11 +519,15 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
}
|
||||
|
||||
++cacheCount_;
|
||||
if (cacheHardCap_ > 0 && cacheCount_ > cacheHardCap_)
|
||||
evictForHardCap(*cit.ait, cit.mit);
|
||||
return true;
|
||||
}
|
||||
|
||||
entry.ptr = data;
|
||||
++cacheCount_;
|
||||
if (cacheHardCap_ > 0 && cacheCount_ > cacheHardCap_)
|
||||
evictForHardCap(*cit.ait, cit.mit);
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -729,6 +837,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
|
||||
{
|
||||
// independent of cache size, so not counted as a hit
|
||||
++cacheCount_;
|
||||
if (cacheHardCap_ > 0 && cacheCount_ > cacheHardCap_)
|
||||
evictForHardCap(*cit.ait, cit.mit);
|
||||
entry.touch(clock_.now());
|
||||
return entry.ptr.getStrong();
|
||||
}
|
||||
|
||||
@@ -22,10 +22,13 @@ struct Sections
|
||||
static constexpr auto kIoWorkers = "io_workers";
|
||||
static constexpr auto kIps = "ips";
|
||||
static constexpr auto kIpsFixed = "ips_fixed";
|
||||
static constexpr auto kLedgerCacheAge = "ledger_cache_age";
|
||||
static constexpr auto kLedgerFetchSize = "ledger_fetch_size";
|
||||
static constexpr auto kLedgerHistory = "ledger_history";
|
||||
static constexpr auto kLedgerReplay = "ledger_replay";
|
||||
static constexpr auto kLedgerTxTables = "ledger_tx_tables";
|
||||
static constexpr auto kMaxTransactions = "max_transactions";
|
||||
static constexpr auto kMemoryLimit = "memory_limit";
|
||||
static constexpr auto kNetworkId = "network_id";
|
||||
static constexpr auto kNetworkQuorum = "network_quorum";
|
||||
static constexpr auto kNodeDatabase = "node_db";
|
||||
@@ -63,6 +66,7 @@ struct Sections
|
||||
static constexpr auto kSslVerifyFile = "ssl_verify_file";
|
||||
static constexpr auto kSweepInterval = "sweep_interval";
|
||||
static constexpr auto kTransactionQueue = "transaction_queue";
|
||||
static constexpr auto kTreeCacheAge = "tree_cache_age";
|
||||
static constexpr auto kValidationSeed = "validation_seed";
|
||||
static constexpr auto kValidatorKeys = "validator_keys";
|
||||
static constexpr auto kValidatorKeyRevocation = "validator_key_revocation";
|
||||
|
||||
@@ -395,6 +395,7 @@ JSS(mean); // out: get_aggregate_price
|
||||
JSS(median); // out: get_aggregate_price
|
||||
JSS(median_fee); // out: TxQ
|
||||
JSS(median_level); // out: TxQ
|
||||
JSS(memory_limit); // out: server_info
|
||||
JSS(message); // error.
|
||||
JSS(meta); // out: NetworkOPs, AccountTx*, Tx
|
||||
JSS(meta_blob); // out: NetworkOPs, AccountTx*, Tx
|
||||
@@ -435,7 +436,6 @@ JSS(node_read_retries); // out: GetCounts
|
||||
JSS(node_reads_hit); // out: GetCounts
|
||||
JSS(node_reads_total); // out: GetCounts
|
||||
JSS(node_reads_duration_us); // out: GetCounts
|
||||
JSS(node_size); // out: server_info
|
||||
JSS(nodes); // out: VaultInfo
|
||||
JSS(nodestore); // out: GetCounts
|
||||
JSS(node_writes); // out: GetCounts
|
||||
|
||||
@@ -502,7 +502,7 @@ public:
|
||||
|
||||
auto backend{node_store::Manager::instance().makeBackend(
|
||||
section,
|
||||
megabytes(env.app().config().getValueFor(SizedItem::BurstSize, std::nullopt)),
|
||||
megabytes(env.app().config().getValueFor(SizedItem::BurstSize)),
|
||||
scheduler,
|
||||
env.app().getJournal("NodeStoreTest"))};
|
||||
backend->open();
|
||||
@@ -524,22 +524,12 @@ public:
|
||||
// Normally, SHAMapStoreImp handles all these details.
|
||||
auto nscfg = env.app().config().section(Sections::kNodeDatabase);
|
||||
|
||||
// Provide default values.
|
||||
// Provide default values (mirrors SHAMapStoreImp::makeNodeStore).
|
||||
if (!nscfg.exists(Keys::kCacheSize))
|
||||
{
|
||||
nscfg.set(
|
||||
Keys::kCacheSize,
|
||||
std::to_string(
|
||||
env.app().config().getValueFor(SizedItem::TreeCacheSize, std::nullopt)));
|
||||
}
|
||||
nscfg.set(Keys::kCacheSize, "16384");
|
||||
|
||||
if (!nscfg.exists(Keys::kCacheAge))
|
||||
{
|
||||
nscfg.set(
|
||||
Keys::kCacheAge,
|
||||
std::to_string(
|
||||
env.app().config().getValueFor(SizedItem::TreeCacheAge, std::nullopt)));
|
||||
}
|
||||
nscfg.set(Keys::kCacheAge, "5");
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
|
||||
|
||||
@@ -596,6 +596,87 @@ main
|
||||
BEAST_EXPECT(c.networkId == 10000);
|
||||
}
|
||||
|
||||
void
|
||||
testMemoryLimit()
|
||||
{
|
||||
testcase("memory limit");
|
||||
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("");
|
||||
BEAST_EXPECT(!c.memoryLimit);
|
||||
}
|
||||
|
||||
auto const parse = [](std::string const& value) {
|
||||
Config c;
|
||||
c.loadFromString("[memory_limit]\n" + value + "\n");
|
||||
return c;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(parse("16").memoryLimit == std::uint64_t{16} << 30);
|
||||
BEAST_EXPECT(parse("0").memoryLimit == std::uint64_t{0});
|
||||
BEAST_EXPECT(parse("0").cacheMemoryBudget() == 0);
|
||||
|
||||
// Garbage and out-of-range values are rejected.
|
||||
expectException([&parse] { parse("banana"); });
|
||||
expectException([&parse] { parse("2000"); });
|
||||
|
||||
// Standalone mode does not change the budget: detected RAM unless
|
||||
// a limit is configured.
|
||||
{
|
||||
Config c;
|
||||
c.setupControl(true, true, true);
|
||||
c.loadFromString("[memory_limit]\n8\n");
|
||||
BEAST_EXPECT(c.cacheMemoryBudget() == std::uint64_t{8} << 30);
|
||||
}
|
||||
|
||||
// Values derive from the budget: half of it at 8 KiB per entry for
|
||||
// the tree cache; 0 yields the floors.
|
||||
BEAST_EXPECT(parse("16").getValueFor(SizedItem::TreeCacheSize) == 1048576);
|
||||
BEAST_EXPECT(parse("64").getValueFor(SizedItem::TreeCacheSize) == 4194304);
|
||||
BEAST_EXPECT(parse("0").getValueFor(SizedItem::TreeCacheSize) == 16384);
|
||||
BEAST_EXPECT(parse("16").getValueFor(SizedItem::TxnDbCache) == 32);
|
||||
BEAST_EXPECT(parse("0").getValueFor(SizedItem::TxnDbCache) == 4);
|
||||
BEAST_EXPECT(parse("16").getValueFor(SizedItem::SweepInterval) == 30);
|
||||
BEAST_EXPECT(parse("16").getValueFor(SizedItem::LedgerSize) == 96);
|
||||
BEAST_EXPECT(parse("16").getValueFor(SizedItem::BurstSize) == 16);
|
||||
BEAST_EXPECT(parse("1024").getValueFor(SizedItem::BurstSize) == 48);
|
||||
|
||||
// Deprecated [node_size] tiers are aliases for budgets (by name,
|
||||
// case-insensitively, or legacy 0-4 index); an explicit
|
||||
// [memory_limit] wins.
|
||||
auto const alias = [](std::string const& value) {
|
||||
Config c;
|
||||
c.loadFromString("[node_size]\n" + value + "\n");
|
||||
return c;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(alias("large").cacheMemoryBudget() == std::uint64_t{64} << 30);
|
||||
BEAST_EXPECT(alias("large").getValueFor(SizedItem::TreeCacheSize) == 4194304);
|
||||
BEAST_EXPECT(alias("HUGE").cacheMemoryBudget() == std::uint64_t{128} << 30);
|
||||
BEAST_EXPECT(alias("3").cacheMemoryBudget() == std::uint64_t{64} << 30);
|
||||
BEAST_EXPECT(alias("9").cacheMemoryBudget() == std::uint64_t{128} << 30);
|
||||
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[node_size]\nsmall\n\n[memory_limit]\n100\n");
|
||||
BEAST_EXPECT(c.cacheMemoryBudget() == std::uint64_t{100} << 30);
|
||||
}
|
||||
|
||||
// Policy values are fixed but individually overridable.
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[tree_cache_age]\n900\n\n[ledger_fetch_size]\n8\n");
|
||||
BEAST_EXPECT(c.getValueFor(SizedItem::TreeCacheAge) == 900);
|
||||
BEAST_EXPECT(c.getValueFor(SizedItem::LedgerFetch) == 8);
|
||||
BEAST_EXPECT(c.getValueFor(SizedItem::LedgerAge) == 180);
|
||||
}
|
||||
expectException([] {
|
||||
Config c;
|
||||
c.loadFromString("[ledger_fetch_size]\n100\n");
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testValidatorsFile()
|
||||
{
|
||||
@@ -1596,6 +1677,7 @@ r.ripple.com:51235
|
||||
testAmendment();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testMemoryLimit();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -301,7 +301,6 @@ public:
|
||||
using namespace std::chrono_literals;
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->fees.referenceFee = 10;
|
||||
cfg->nodeSize = 0;
|
||||
return cfg;
|
||||
})};
|
||||
Account const gw{"gateway"};
|
||||
|
||||
@@ -78,6 +78,8 @@ admin = 127.0.0.1
|
||||
BEAST_EXPECT(result.isMember(jss::info));
|
||||
auto const& info = result[jss::info];
|
||||
BEAST_EXPECT(info.isMember(jss::build_version));
|
||||
// Admin request: reports the cache memory budget in GB.
|
||||
BEAST_EXPECT(info.isMember(jss::memory_limit));
|
||||
// Git info is not guaranteed to be present
|
||||
if (info.isMember(jss::git))
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/basics/IntrusiveRefCounts.h>
|
||||
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/insight/NullCollector.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
@@ -243,4 +244,57 @@ TEST(TaggedCacheTest, tagged_cache)
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TaggedCacheTest, hard_cap_enforced_on_insert)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
beast::Journal const journal{TestSink::instance()};
|
||||
|
||||
TestStopwatch clock;
|
||||
clock.set(0);
|
||||
|
||||
using Key = LedgerIndex;
|
||||
using Value = std::string;
|
||||
using Cache = TaggedCache<Key, Value>;
|
||||
|
||||
// A cap-enabled cache must never let the strong-cache count exceed the
|
||||
// cap, enforced on the insert path alone (no sweep). The large targetSize
|
||||
// and long age make the periodic sweep irrelevant here, so only
|
||||
// evictForHardCap can be bounding it.
|
||||
int const cap = 100;
|
||||
Cache capped(
|
||||
"capped", 1'000'000, 3600s, clock, journal, beast::insight::NullCollector::make(), cap);
|
||||
|
||||
bool everExceeded = false;
|
||||
for (Key k = 1; k <= 1000; ++k)
|
||||
{
|
||||
capped.insert(k, "v");
|
||||
if (capped.getCacheSize() > cap)
|
||||
everExceeded = true;
|
||||
}
|
||||
EXPECT_FALSE(everExceeded);
|
||||
EXPECT_LE(capped.getCacheSize(), cap);
|
||||
EXPECT_GT(capped.getCacheSize(), 0);
|
||||
}
|
||||
|
||||
TEST(TaggedCacheTest, hard_cap_disabled)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
beast::Journal const journal{TestSink::instance()};
|
||||
|
||||
TestStopwatch clock;
|
||||
clock.set(0);
|
||||
|
||||
using Key = LedgerIndex;
|
||||
using Value = std::string;
|
||||
using Cache = TaggedCache<Key, Value>;
|
||||
|
||||
// cacheHardCap = 0: growth is bounded only by the periodic sweep.
|
||||
Cache uncapped(
|
||||
"uncapped", 1'000'000, 3600s, clock, journal, beast::insight::NullCollector::make(), 0);
|
||||
|
||||
for (Key k = 1; k <= 1000; ++k)
|
||||
uncapped.insert(k, "v");
|
||||
EXPECT_EQ(uncapped.getCacheSize(), 1000);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -292,9 +292,8 @@ public:
|
||||
|
||||
auto const cores = std::thread::hardware_concurrency();
|
||||
|
||||
// Use a single thread when running on under-provisioned systems
|
||||
// or if we are configured to use minimal resources.
|
||||
if ((cores == 1) || ((config.nodeSize == 0) && (cores == 2)))
|
||||
// Use a single thread on under-provisioned systems.
|
||||
if (cores <= 2)
|
||||
return 1;
|
||||
|
||||
// Otherwise, prefer six threads.
|
||||
@@ -336,14 +335,12 @@ public:
|
||||
|
||||
auto count = static_cast<int>(std::thread::hardware_concurrency());
|
||||
|
||||
// Be more aggressive about the number of threads to use
|
||||
// for the job queue if the server is configured as
|
||||
// "large" or "huge" if there are enough cores.
|
||||
if (config->nodeSize >= 4 && count >= 16)
|
||||
// Scale the job queue with the available cores.
|
||||
if (count >= 16)
|
||||
{
|
||||
count = 6 + std::min(count, 8);
|
||||
}
|
||||
else if (config->nodeSize >= 3 && count >= 8)
|
||||
else if (count >= 8)
|
||||
{
|
||||
count = 4 + std::min(count, 6);
|
||||
}
|
||||
@@ -863,7 +860,7 @@ public:
|
||||
node_store::DummyScheduler dummyScheduler;
|
||||
std::unique_ptr<node_store::Database> source =
|
||||
node_store::Manager::instance().makeDatabase(
|
||||
megabytes(config_->getValueFor(SizedItem::BurstSize, std::nullopt)),
|
||||
megabytes(config_->getValueFor(SizedItem::BurstSize)),
|
||||
dummyScheduler,
|
||||
0,
|
||||
config_->section(Sections::kImportNodeDatabase),
|
||||
|
||||
@@ -2703,27 +2703,9 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
|
||||
|
||||
if (admin)
|
||||
{
|
||||
// Note: By default the node size is "tiny". When parsing it's an error if the final
|
||||
// NODE_SIZE is over 4 so below code should be safe.
|
||||
// NOLINTNEXTLINE(bugprone-switch-missing-default-case)
|
||||
switch (registry_.get().getApp().config().nodeSize)
|
||||
{
|
||||
case 0:
|
||||
info[jss::node_size] = "tiny";
|
||||
break;
|
||||
case 1:
|
||||
info[jss::node_size] = "small";
|
||||
break;
|
||||
case 2:
|
||||
info[jss::node_size] = "medium";
|
||||
break;
|
||||
case 3:
|
||||
info[jss::node_size] = "large";
|
||||
break;
|
||||
case 4:
|
||||
info[jss::node_size] = "huge";
|
||||
break;
|
||||
}
|
||||
// The cache memory budget in GB; 0 means enforcement is disabled.
|
||||
info[jss::memory_limit] =
|
||||
static_cast<json::UInt>(registry_.get().getApp().config().cacheMemoryBudget() >> 30);
|
||||
|
||||
auto when = registry_.get().getValidators().expires();
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ SHAMapStoreImp::SHAMapStoreImp(
|
||||
Keys::kCacheMb, std::to_string(config.getValueFor(SizedItem::HashNodeDbCache)));
|
||||
}
|
||||
|
||||
if (!section.exists(Keys::kFilterBits) && (config.nodeSize >= 2))
|
||||
if (!section.exists(Keys::kFilterBits) && config.cacheMemoryBudget() != 0)
|
||||
section.set(Keys::kFilterBits, "10");
|
||||
}
|
||||
|
||||
@@ -170,20 +170,13 @@ SHAMapStoreImp::makeNodeStore(int readThreads)
|
||||
{
|
||||
auto nscfg = app_.config().section(Sections::kNodeDatabase);
|
||||
|
||||
// Provide default values.
|
||||
// Documented defaults: 16384 records, 5 minutes (DatabaseNodeImp reads
|
||||
// cache_age in minutes).
|
||||
if (!nscfg.exists(Keys::kCacheSize))
|
||||
{
|
||||
nscfg.set(
|
||||
Keys::kCacheSize,
|
||||
std::to_string(app_.config().getValueFor(SizedItem::TreeCacheSize, std::nullopt)));
|
||||
}
|
||||
nscfg.set(Keys::kCacheSize, "16384");
|
||||
|
||||
if (!nscfg.exists(Keys::kCacheAge))
|
||||
{
|
||||
nscfg.set(
|
||||
Keys::kCacheAge,
|
||||
std::to_string(app_.config().getValueFor(SizedItem::TreeCacheAge, std::nullopt)));
|
||||
}
|
||||
nscfg.set(Keys::kCacheAge, "5");
|
||||
|
||||
std::unique_ptr<node_store::Database> db;
|
||||
|
||||
@@ -215,7 +208,7 @@ SHAMapStoreImp::makeNodeStore(int readThreads)
|
||||
else
|
||||
{
|
||||
db = node_store::Manager::instance().makeDatabase(
|
||||
megabytes(app_.config().getValueFor(SizedItem::BurstSize, std::nullopt)),
|
||||
megabytes(app_.config().getValueFor(SizedItem::BurstSize)),
|
||||
scheduler_,
|
||||
readThreads,
|
||||
nscfg,
|
||||
@@ -537,7 +530,7 @@ SHAMapStoreImp::makeBackendRotating(std::string path)
|
||||
|
||||
auto backend{node_store::Manager::instance().makeBackend(
|
||||
section,
|
||||
megabytes(app_.config().getValueFor(SizedItem::BurstSize, std::nullopt)),
|
||||
megabytes(app_.config().getValueFor(SizedItem::BurstSize)),
|
||||
scheduler_,
|
||||
app_.getJournal(kNodeStoreName))};
|
||||
backend->open();
|
||||
|
||||
@@ -38,9 +38,7 @@ enum class SizedItem : std::size_t {
|
||||
HashNodeDbCache,
|
||||
TxnDbCache,
|
||||
LgrDbCache,
|
||||
OpenFinalLimit,
|
||||
BurstSize,
|
||||
RamSizeGb,
|
||||
AccountIdCacheSize,
|
||||
};
|
||||
|
||||
@@ -138,7 +136,8 @@ private:
|
||||
*/
|
||||
bool signingEnabled_ = false;
|
||||
|
||||
// The amount of RAM, in bytes, that we detected on this system.
|
||||
// The amount of RAM, in GiB, that we detected on this system.
|
||||
// 0 when detection failed.
|
||||
std::uint64_t const ramSize_;
|
||||
|
||||
public:
|
||||
@@ -209,10 +208,10 @@ public:
|
||||
std::uint32_t ledgerHistory = 256;
|
||||
std::uint32_t fetchDepth = 1000000000;
|
||||
|
||||
// Tunable that adjusts various parameters, typically associated
|
||||
// with hardware parameters (RAM size and CPU cores). The default
|
||||
// is 'tiny'.
|
||||
std::size_t nodeSize = 0;
|
||||
// Cache memory budget in bytes, from [memory_limit] (gigabytes). Unset
|
||||
// defaults to detected physical RAM; 0 disables enforcement. The
|
||||
// deprecated [node_size] tiers map onto this budget.
|
||||
std::optional<std::uint64_t> memoryLimit;
|
||||
|
||||
bool sslVerify = true;
|
||||
std::string sslVerifyFile;
|
||||
@@ -244,6 +243,11 @@ public:
|
||||
// size, but we allow admins to explicitly set it in the config.
|
||||
std::optional<int> sweepInterval;
|
||||
|
||||
// Optional overrides for the fixed cache policy values.
|
||||
std::optional<int> treeCacheAge; // [tree_cache_age], seconds
|
||||
std::optional<int> ledgerCacheAge; // [ledger_cache_age], seconds
|
||||
std::optional<int> ledgerFetchSize; // [ledger_fetch_size], ledgers per fetch pass
|
||||
|
||||
// Reduce-relay - Experimental parameters to control p2p routing algorithms
|
||||
|
||||
// Enable base squelching of duplicate validation/proposal messages
|
||||
@@ -352,25 +356,22 @@ public:
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the default value for the item at the specified node size
|
||||
*
|
||||
* @param item The item for which the default value is needed
|
||||
* @param node Optional value, used to adjust the result to match the
|
||||
* size of a node (0: tiny, ..., 4: huge). If unseated,
|
||||
* uses the configured size (NODE_SIZE).
|
||||
*
|
||||
* @throws This method can throw std::out_of_range if you ask for values
|
||||
* that it does not recognize or request a non-default node-size.
|
||||
* Retrieve the value for the item, derived from the memory budget.
|
||||
*
|
||||
* @param item The item for which the value is needed
|
||||
* @return The value for the requested item.
|
||||
*
|
||||
* @note The defaults are selected so as to be reasonable, but the node
|
||||
* size is an imprecise metric that combines multiple aspects of
|
||||
* the underlying system; this means that we can't provide optimal
|
||||
* defaults in the code for every case.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getValueFor(SizedItem item, std::optional<std::size_t> node = std::nullopt) const;
|
||||
getValueFor(SizedItem item) const;
|
||||
|
||||
/**
|
||||
* The effective cache memory budget in bytes.
|
||||
*
|
||||
* [memory_limit] if set, otherwise detected physical RAM. 0 means
|
||||
* enforcement is disabled (explicit 0, or RAM detection failed).
|
||||
*/
|
||||
[[nodiscard]] std::uint64_t
|
||||
cacheMemoryBudget() const;
|
||||
|
||||
[[nodiscard]] beast::Journal
|
||||
journal() const
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -45,7 +44,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -71,15 +70,144 @@ getMemorySize()
|
||||
#if BOOST_OS_LINUX
|
||||
#include <sys/sysinfo.h> // IWYU pragma: keep
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace xrpl::detail {
|
||||
|
||||
// This process's cgroup path from /proc/self/cgroup: the v2 line is
|
||||
// "0::<path>"; a v1 line is "<id>:<controllers>:<path>". Empty when absent.
|
||||
[[nodiscard]] std::string
|
||||
getOwnCgroupPath(std::string_view controller)
|
||||
{
|
||||
std::ifstream in("/proc/self/cgroup");
|
||||
std::string line;
|
||||
|
||||
while (std::getline(in, line))
|
||||
{
|
||||
auto const first = line.find(':');
|
||||
auto const second = line.find(':', first + 1);
|
||||
if (first == std::string::npos || second == std::string::npos)
|
||||
continue;
|
||||
|
||||
std::string_view const controllers(line.data() + first + 1, second - first - 1);
|
||||
if (controller.empty())
|
||||
{
|
||||
// The v2 entry is exactly "0::<path>".
|
||||
if (first == 1 && line[0] == '0' && controllers.empty())
|
||||
return line.substr(second + 1);
|
||||
}
|
||||
else if (controllers.contains(controller))
|
||||
{
|
||||
return line.substr(second + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// The value in a cgroup limit file; 0 when absent or unlimited. "max" (v2)
|
||||
// fails the read, and the page-counter maximum (v1) both mean unlimited.
|
||||
[[nodiscard]] std::uint64_t
|
||||
readCgroupLimit(std::string const& path)
|
||||
{
|
||||
std::ifstream in(path);
|
||||
std::uint64_t limit = 0;
|
||||
|
||||
if (in >> limit && limit < (std::uint64_t{1} << 62))
|
||||
return limit;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Whether the cgroup directory contains this process. /proc/self/cgroup
|
||||
// paths are namespace-relative, so a resolved directory can name-collide
|
||||
// with a different cgroup when the cgroup mount shows another view; only
|
||||
// trust a directory this process is actually in.
|
||||
[[nodiscard]] bool
|
||||
cgroupContainsSelf(std::string const& dir)
|
||||
{
|
||||
std::ifstream in(dir + "/cgroup.procs");
|
||||
pid_t const self = ::getpid();
|
||||
pid_t pid = 0;
|
||||
|
||||
while (in >> pid)
|
||||
{
|
||||
if (pid == self)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// The smallest numeric limit in `file` from the leaf cgroup up through its
|
||||
// ancestors (the effective limit is the minimum over the hierarchy); 0 when
|
||||
// none is set or the leaf does not belong to this process.
|
||||
[[nodiscard]] std::uint64_t
|
||||
minCgroupLimit(std::string const& mount, std::string path, char const* file)
|
||||
{
|
||||
if (!cgroupContainsSelf(mount + path))
|
||||
return 0;
|
||||
|
||||
std::uint64_t best = 0;
|
||||
auto const consider = [&best](std::uint64_t limit) {
|
||||
if (limit != 0 && (best == 0 || limit < best))
|
||||
best = limit;
|
||||
};
|
||||
|
||||
while (!path.empty() && path != "/")
|
||||
{
|
||||
consider(readCgroupLimit(mount + path + "/" + file));
|
||||
|
||||
auto const slash = path.find_last_of('/');
|
||||
if (slash == std::string::npos)
|
||||
break;
|
||||
path.resize(slash);
|
||||
}
|
||||
consider(readCgroupLimit(mount + "/" + file));
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
// The cgroup (v2, then v1) memory limit in bytes; 0 when absent or
|
||||
// unlimited. Checks this process's own cgroup and its ancestors (covering
|
||||
// nested limits such as systemd MemoryMax=) before the root-level files
|
||||
// containers expose.
|
||||
[[nodiscard]] std::uint64_t
|
||||
getCgroupMemoryLimit()
|
||||
{
|
||||
if (auto const path = getOwnCgroupPath(""); !path.empty() && path != "/")
|
||||
{
|
||||
if (auto const limit = minCgroupLimit("/sys/fs/cgroup", path, "memory.max"))
|
||||
return limit;
|
||||
}
|
||||
|
||||
if (auto const limit = readCgroupLimit("/sys/fs/cgroup/memory.max"))
|
||||
return limit;
|
||||
|
||||
if (auto const path = getOwnCgroupPath("memory"); !path.empty() && path != "/")
|
||||
{
|
||||
if (auto const limit =
|
||||
minCgroupLimit("/sys/fs/cgroup/memory", path, "memory.limit_in_bytes"))
|
||||
return limit;
|
||||
}
|
||||
|
||||
return readCgroupLimit("/sys/fs/cgroup/memory/memory.limit_in_bytes");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint64_t
|
||||
getMemorySize()
|
||||
{
|
||||
if (struct sysinfo si{}; sysinfo(&si) == 0)
|
||||
return static_cast<std::uint64_t>(si.totalram) * si.mem_unit;
|
||||
std::uint64_t ram = 0;
|
||||
|
||||
return 0;
|
||||
if (struct sysinfo si{}; sysinfo(&si) == 0)
|
||||
ram = static_cast<std::uint64_t>(si.totalram) * si.mem_unit;
|
||||
|
||||
if (auto const limit = getCgroupMemoryLimit(); limit != 0 && (ram == 0 || limit < ram))
|
||||
return limit;
|
||||
|
||||
return ram;
|
||||
}
|
||||
|
||||
} // namespace xrpl::detail
|
||||
@@ -110,50 +238,6 @@ getMemorySize()
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// clang-format off
|
||||
// The configurable node sizes are "tiny", "small", "medium", "large", "huge"
|
||||
inline constexpr std::array<std::pair<SizedItem, std::array<int, 5>>, 13>
|
||||
kSizedItems
|
||||
{{
|
||||
// FIXME: We should document each of these items, explaining exactly
|
||||
// what they control and whether there exists an explicit
|
||||
// config option that can be used to override the default.
|
||||
|
||||
// tiny small medium large huge
|
||||
{SizedItem::SweepInterval, {{ 10, 30, 60, 90, 120 }}},
|
||||
{SizedItem::TreeCacheSize, {{ 262144, 524288, 2097152, 4194304, 8388608 }}},
|
||||
{SizedItem::TreeCacheAge, {{ 30, 60, 90, 120, 900 }}},
|
||||
{SizedItem::LedgerSize, {{ 32, 32, 64, 256, 384 }}},
|
||||
{SizedItem::LedgerAge, {{ 30, 60, 180, 300, 600 }}},
|
||||
{SizedItem::LedgerFetch, {{ 2, 3, 4, 5, 8 }}},
|
||||
{SizedItem::HashNodeDbCache, {{ 4, 12, 24, 64, 128 }}},
|
||||
{SizedItem::TxnDbCache, {{ 4, 12, 24, 64, 128 }}},
|
||||
{SizedItem::LgrDbCache, {{ 4, 8, 16, 32, 128 }}},
|
||||
{SizedItem::OpenFinalLimit, {{ 8, 16, 32, 64, 128 }}},
|
||||
{SizedItem::BurstSize, {{ 4, 8, 16, 32, 48 }}},
|
||||
{SizedItem::RamSizeGb, {{ 6, 8, 12, 24, 0 }}},
|
||||
{SizedItem::AccountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}}
|
||||
}};
|
||||
// clang-format on
|
||||
|
||||
// Ensure that the order of entries in the table corresponds to the
|
||||
// order of entries in the enum:
|
||||
static_assert(
|
||||
[]() constexpr -> bool {
|
||||
std::underlying_type_t<SizedItem> idx = 0;
|
||||
|
||||
for (auto const& i : kSizedItems)
|
||||
{
|
||||
if (static_cast<std::underlying_type_t<SizedItem>>(i.first) != idx)
|
||||
return false;
|
||||
|
||||
++idx;
|
||||
}
|
||||
|
||||
return true;
|
||||
}(),
|
||||
"Mismatch between sized item enum & array indices");
|
||||
|
||||
//
|
||||
// TODO: Check permissions on config file before using it.
|
||||
//
|
||||
@@ -271,36 +355,9 @@ Config::Config()
|
||||
void
|
||||
Config::setupControl(bool bQuiet, bool bSilent, bool bStandalone)
|
||||
{
|
||||
XRPL_ASSERT(nodeSize == 0, "xrpl::Config::setupControl : node size not set");
|
||||
|
||||
quiet_ = bQuiet || bSilent;
|
||||
silent_ = bSilent;
|
||||
runStandalone_ = bStandalone;
|
||||
|
||||
// We try to autodetect the appropriate node size by checking available
|
||||
// RAM and CPU resources. We default to "tiny" for standalone mode.
|
||||
if (!bStandalone)
|
||||
{
|
||||
// First, check against 'minimum' RAM requirements per node size:
|
||||
auto const& threshold =
|
||||
kSizedItems[std::underlying_type_t<SizedItem>(SizedItem::RamSizeGb)];
|
||||
|
||||
auto ns = std::ranges::find_if(threshold.second, [this](std::size_t limit) {
|
||||
return (limit == 0) || (ramSize_ < limit);
|
||||
});
|
||||
|
||||
XRPL_ASSERT(ns != threshold.second.end(), "xrpl::Config::setupControl : valid node size");
|
||||
|
||||
if (ns != threshold.second.end())
|
||||
nodeSize = std::distance(threshold.second.begin(), ns);
|
||||
|
||||
// Adjust the size based on the number of hardware threads of
|
||||
// execution available to us:
|
||||
if (auto const hc = std::thread::hardware_concurrency(); hc != 0)
|
||||
nodeSize = std::min<std::size_t>(hc / 2, nodeSize);
|
||||
}
|
||||
|
||||
XRPL_ASSERT(nodeSize <= 4, "xrpl::Config::setupControl : node size is set");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -583,34 +640,55 @@ Config::loadFromString(std::string const& fileContents)
|
||||
}
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kMemoryLimit, strTemp, j_))
|
||||
{
|
||||
// Gigabytes; 0 disables enforcement.
|
||||
auto const gb = beast::lexicalCastThrow<std::uint64_t>(strTemp);
|
||||
if (gb > 1024)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid value '" + strTemp + "' for key '" + Sections::kMemoryLimit +
|
||||
"'; the limit is in gigabytes and may not exceed 1024");
|
||||
}
|
||||
memoryLimit = gb << 30;
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kNodeSize, strTemp, j_))
|
||||
{
|
||||
if (boost::iequals(strTemp, "tiny"))
|
||||
// Deprecated: each tier (by name or its legacy 0-4 index) is an
|
||||
// alias for a memory budget. [memory_limit], when present, wins.
|
||||
static constexpr std::array<std::pair<std::string_view, std::uint64_t>, 5> kTiers{
|
||||
{{"tiny", 4}, {"small", 8}, {"medium", 32}, {"large", 64}, {"huge", 128}}};
|
||||
|
||||
auto const tier = std::ranges::find_if(
|
||||
kTiers, [&strTemp](auto const& t) { return boost::iequals(strTemp, t.first); });
|
||||
|
||||
std::uint64_t const budgetGb = tier != kTiers.end()
|
||||
? tier->second
|
||||
: kTiers[std::min<std::size_t>(4, beast::lexicalCastThrow<std::size_t>(strTemp))]
|
||||
.second;
|
||||
|
||||
if (!memoryLimit)
|
||||
memoryLimit = budgetGb << 30;
|
||||
|
||||
if (!quiet_)
|
||||
{
|
||||
nodeSize = 0;
|
||||
}
|
||||
else if (boost::iequals(strTemp, "small"))
|
||||
{
|
||||
nodeSize = 1;
|
||||
}
|
||||
else if (boost::iequals(strTemp, "medium"))
|
||||
{
|
||||
nodeSize = 2;
|
||||
}
|
||||
else if (boost::iequals(strTemp, "large"))
|
||||
{
|
||||
nodeSize = 3;
|
||||
}
|
||||
else if (boost::iequals(strTemp, "huge"))
|
||||
{
|
||||
nodeSize = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
nodeSize = std::min<std::size_t>(4, beast::lexicalCastThrow<std::size_t>(strTemp));
|
||||
std::cerr << "WARNING: [node_size] is deprecated and will be removed "
|
||||
"in a future release. Set [memory_limit] instead; thread "
|
||||
"counts derive from the core count and [workers] / "
|
||||
"[io_workers].\n";
|
||||
}
|
||||
}
|
||||
|
||||
// A budget beyond physical memory cannot be honored and recreates the
|
||||
// oversized-preset OOM this setting exists to prevent.
|
||||
if (memoryLimit && ramSize_ != 0 && *memoryLimit > (ramSize_ << 30) && !quiet_)
|
||||
{
|
||||
std::cerr << "WARNING: the configured memory budget (" << (*memoryLimit >> 30)
|
||||
<< " GB) exceeds detected RAM (" << ramSize_ << " GB); set [memory_limit] to "
|
||||
<< ramSize_ << " or less.\n";
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kSigningSupport, strTemp, j_))
|
||||
signingEnabled_ = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
@@ -747,6 +825,42 @@ Config::loadFromString(std::string const& fileContents)
|
||||
}
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kTreeCacheAge, strTemp, j_))
|
||||
{
|
||||
treeCacheAge = beast::lexicalCastThrow<int>(strTemp);
|
||||
|
||||
if (*treeCacheAge < 10 || *treeCacheAge > 3600)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid ") + Sections::kTreeCacheAge +
|
||||
": must be between 10 and 3600 inclusive");
|
||||
}
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kLedgerCacheAge, strTemp, j_))
|
||||
{
|
||||
ledgerCacheAge = beast::lexicalCastThrow<int>(strTemp);
|
||||
|
||||
if (*ledgerCacheAge < 10 || *ledgerCacheAge > 3600)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid ") + Sections::kLedgerCacheAge +
|
||||
": must be between 10 and 3600 inclusive");
|
||||
}
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kLedgerFetchSize, strTemp, j_))
|
||||
{
|
||||
ledgerFetchSize = beast::lexicalCastThrow<int>(strTemp);
|
||||
|
||||
if (*ledgerFetchSize < 1 || *ledgerFetchSize > 16)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid ") + Sections::kLedgerFetchSize +
|
||||
": must be between 1 and 16 inclusive");
|
||||
}
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, Sections::kWorkers, strTemp, j_))
|
||||
{
|
||||
workers = beast::lexicalCastThrow<int>(strTemp);
|
||||
@@ -1195,12 +1309,57 @@ Config::getDebugLogFile() const
|
||||
}
|
||||
|
||||
int
|
||||
Config::getValueFor(SizedItem item, std::optional<std::size_t> node) const
|
||||
Config::getValueFor(SizedItem item) const
|
||||
{
|
||||
auto const index = static_cast<std::underlying_type_t<SizedItem>>(item);
|
||||
XRPL_ASSERT(index < kSizedItems.size(), "xrpl::Config::getValueFor : valid index input");
|
||||
XRPL_ASSERT(!node || *node <= 4, "xrpl::Config::getValueFor : unset or valid node");
|
||||
return kSizedItems.at(index).second.at(node.value_or(nodeSize));
|
||||
// Memory-shaped items scale linearly with the budget between a floor and
|
||||
// a ceiling; time and policy items are fixed. A budget of 0 (enforcement
|
||||
// disabled) yields the floors. The 1024 bound keeps gb * 65536 within
|
||||
// int range (the config parser enforces it too).
|
||||
auto const gb = static_cast<int>(std::min<std::uint64_t>(cacheMemoryBudget() >> 30, 1024));
|
||||
|
||||
switch (item)
|
||||
{
|
||||
case SizedItem::SweepInterval:
|
||||
return 30;
|
||||
case SizedItem::TreeCacheSize:
|
||||
// Half the budget at an estimated 8 KiB per entry (the node plus
|
||||
// its weak-tracking entry, hash buckets, and control block):
|
||||
// 1 GiB / 2 / 8 KiB = 65536 entries per budget GB.
|
||||
return std::max(16384, gb * 65536);
|
||||
case SizedItem::TreeCacheAge:
|
||||
return treeCacheAge.value_or(300);
|
||||
case SizedItem::LedgerSize:
|
||||
return std::clamp(gb * 6, 32, 384);
|
||||
case SizedItem::LedgerAge:
|
||||
return ledgerCacheAge.value_or(180);
|
||||
case SizedItem::LedgerFetch:
|
||||
return ledgerFetchSize.value_or(4);
|
||||
case SizedItem::HashNodeDbCache:
|
||||
case SizedItem::TxnDbCache:
|
||||
case SizedItem::LgrDbCache:
|
||||
// HashNodeDbCache is consumed in MB (RocksDB cache_mb); the two
|
||||
// SQLite page caches are consumed in KB.
|
||||
return std::clamp(gb * 2, 4, 128);
|
||||
case SizedItem::BurstSize:
|
||||
return std::clamp(gb, 4, 48);
|
||||
case SizedItem::AccountIdCacheSize:
|
||||
// Fixed regardless of budget: ~22 MB at 72 bytes per slot, and
|
||||
// the value stays prime for hash distribution.
|
||||
return 300007;
|
||||
}
|
||||
|
||||
UNREACHABLE("xrpl::Config::getValueFor : invalid item");
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
Config::cacheMemoryBudget() const
|
||||
{
|
||||
if (memoryLimit)
|
||||
return *memoryLimit;
|
||||
|
||||
// ramSize_ is in GiB; 0 when detection failed, which disables enforcement.
|
||||
return ramSize_ << 30;
|
||||
}
|
||||
|
||||
FeeSetup
|
||||
|
||||
@@ -39,8 +39,17 @@ NodeFamily::NodeFamily(Application& app, CollectorManager& cm)
|
||||
app.config().getValueFor(SizedItem::TreeCacheSize),
|
||||
std::chrono::seconds(app.config().getValueFor(SizedItem::TreeCacheAge)),
|
||||
stopwatch(),
|
||||
j_))
|
||||
j_,
|
||||
beast::insight::NullCollector::make(),
|
||||
// Hard cap: the clamped target, enforced on insert; 0 = off.
|
||||
app.config().cacheMemoryBudget() != 0
|
||||
? app.config().getValueFor(SizedItem::TreeCacheSize)
|
||||
: 0))
|
||||
{
|
||||
auto const budget = app.config().cacheMemoryBudget();
|
||||
JLOG(j_.info()) << "TreeNodeCache sizing: target="
|
||||
<< app.config().getValueFor(SizedItem::TreeCacheSize) << " entries, budget "
|
||||
<< (budget >> 30) << " GB" << (budget == 0 ? " (enforcement disabled)" : "");
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user