Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions API-CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
51 changes: 34 additions & 17 deletions cfg/xrpld-example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
# 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:
#
# | | 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 |
# 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.
#
# [memory_limit]
#
# 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]
#
Expand Down
33 changes: 32 additions & 1 deletion include/xrpl/basics/TaggedCache.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,20 @@ class TaggedCache
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:
/**
Expand Down Expand Up @@ -357,6 +364,15 @@ class TaggedCache

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(
typename cache_type::map_type& partition,
typename cache_type::map_type::iterator const& keep);

[[nodiscard]] std::thread
sweepHelper(
clock_type::time_point const& whenExpire,
Expand Down Expand Up @@ -390,8 +406,23 @@ class TaggedCache
// 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};
Expand Down
116 changes: 111 additions & 5 deletions include/xrpl/basics/TaggedCache.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
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_(
Expand All @@ -67,6 +68,7 @@
, name_(name)
, targetSize_(size)
, targetAge_(expiration)
, cacheHardCap_(cacheHardCap)
{
}

Expand Down Expand Up @@ -219,6 +221,98 @@
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(
typename cache_type::map_type& partition,
typename 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;

Check warning on line 245 in include/xrpl/basics/TaggedCache.ipp

View check run for this annotation

Codecov / codecov/patch

include/xrpl/basics/TaggedCache.ipp#L245

Added line #L245 was not covered by tests

// 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;

Check warning on line 279 in include/xrpl/basics/TaggedCache.ipp

View check run for this annotation

Codecov / codecov/patch

include/xrpl/basics/TaggedCache.ipp#L279

Added line #L279 was not covered by tests
}
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;

Check warning on line 290 in include/xrpl/basics/TaggedCache.ipp

View check run for this annotation

Codecov / codecov/patch

include/xrpl/basics/TaggedCache.ipp#L290

Added line #L290 was not covered by tests
}

auto oldest = partition.find(oldestKey);
if (oldest == partition.end() || oldest == keep || oldest->second.isWeak())
return;

Check warning on line 295 in include/xrpl/basics/TaggedCache.ipp

View check run for this annotation

Codecov / codecov/patch

include/xrpl/basics/TaggedCache.ipp#L295

Added line #L295 was not covered by tests

if (oldest->second.ptr.useCount() == 1)
partition.erase(oldest); // sole owner: release entirely
else
oldest->second.ptr.convertToWeak(); // others hold it: keep weak
--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,
Expand Down Expand Up @@ -360,11 +454,17 @@

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;
}

Expand Down Expand Up @@ -415,11 +515,15 @@
}

++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;
}
Expand Down Expand Up @@ -729,6 +833,8 @@
{
// 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();
}
Expand Down
4 changes: 4 additions & 0 deletions include/xrpl/config/Constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion include/xrpl/protocol/jss.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 4 additions & 14 deletions src/test/app/SHAMapStore_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ class SHAMapStore_test : public beast::unit_test::Suite

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();
Expand All @@ -524,22 +524,12 @@ class SHAMapStore_test : public beast::unit_test::Suite
// 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());

Expand Down
Loading
Loading