Compare commits

..
Author SHA1 Message Date
Mayukha Vadari 1aae015656 fix: Address levelization and clang-tidy errors
Update the levelization results for the new test.rpc > test.unit_test
dependency. Fix the clang-tidy errors in TxQ.h and the config_reload
test: FeeMetrics now calls reconfigure() from its constructor, and the
separate setLimits() helper is removed.
2026-10-21 15:00:36 -04:00
Mayukha Vadari 92e98a3ec9 docs: Add PR link to config_reload changelog entry 2026-10-21 14:23:13 -04:00
Mayukha Vadari f1e56f0166 feat: Add config_reload admin RPC
Add an admin method that reads the config file again and applies the
requested sections without a restart. Only [transaction_queue] can be
reloaded now. A request that names any other section returns
invalidParams and applies nothing. The method also checks the whole
file before it applies anything.

To support this, TxQ gets a reconfigure() method that replaces its
setup parameters under the queue mutex.
2026-10-21 14:19:24 -04:00
15 changed files with 416 additions and 18 deletions

No files matched your search

@@ -156,6 +156,7 @@ test.protocol > xrpld.core
test.protocol > xrpl.json
test.protocol > xrpl.protocol
test.rpc > test.jtx
test.rpc > test.unit_test
test.rpc > xrpl.basics
test.rpc > xrpl.config
test.rpc > xrpl.core
+1
View File
@@ -32,6 +32,7 @@ Version 3.5.0 is not yet released.
### Additions in 3.5.0
- `config_reload`: New admin method. It reads the config file again and applies the sections in the required `sections` field, without a restart. Only `transaction_queue` can be reloaded now. If `sections` names a section that requires a restart, the method returns `invalidParams`. If the file cannot be read or is not valid, the method returns an error. In both cases, the server applies nothing. The `applied` field lists the sections that were applied. Command line: `config_reload <section> [<section> ...]`. ([#8630](https://github.com/XRPLF/rippled/pull/8630))
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive the same `transaction` message as the `transactions` stream for each validated transaction whose metadata affects a subscribed issuance. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
+1
View File
@@ -537,6 +537,7 @@ JSS(search_depth); // in: RipplePathFind
JSS(searched_all); // out: Tx
JSS(secret); // in: TransactionSign, ValidationCreate, ValidationSeed,
// channel_authorize
JSS(sections); // in: ConfigReload
JSS(seed); //
JSS(seed_hex); // in: WalletPropose, TransactionSign
JSS(send_currencies); // out: AccountCurrencies
+37
View File
@@ -615,6 +615,42 @@ public:
checkMetrics(*this, env, 0, 16, 6, 8);
}
void
testReconfigure()
{
testcase("Reconfigure");
using namespace jtx;
Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}}));
auto& txq = env.app().getTxQ();
checkMetrics(*this, env, 0, std::nullopt, 0, 3);
// The maximum queue size is not set before the first ledger closes.
auto setup = setupTxQ(env.app().config());
setup.minimumTxnInLedgerSA = 4;
txq.reconfigure(setup);
checkMetrics(*this, env, 0, std::nullopt, 0, 4);
env.close();
checkMetrics(*this, env, 0, 8, 0, 4);
// New limits apply before the next ledger closes.
setup.minimumTxnInLedgerSA = 5;
setup.ledgersInQueue = 3;
txq.reconfigure(setup);
checkMetrics(*this, env, 0, 15, 0, 5);
// A lower minimum keeps the current expected count.
setup.minimumTxnInLedgerSA = 2;
txq.reconfigure(setup);
checkMetrics(*this, env, 0, 15, 0, 5);
// The minimum queue size still applies.
setup.queueSizeMin = 20;
txq.reconfigure(setup);
checkMetrics(*this, env, 0, 20, 0, 5);
}
void
testTecResult()
{
@@ -4740,6 +4776,7 @@ public:
testQueueSeq();
testQueueTicket();
testTecResult();
testReconfigure();
testLocalTxRetry();
testLastLedgerSeq();
testZeroFeeTxn();
+151
View File
@@ -0,0 +1,151 @@
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <test/unit_test/FileDirGuard.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/core/Config.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <filesystem>
#include <fstream>
#include <ios>
#include <memory>
#include <string>
namespace xrpl {
class ConfigReload_test : public beast::unit_test::Suite
{
static constexpr auto kIpsMessage =
"Section 'ips' in 'sections' cannot be reloaded without a restart. Sections that can be "
"reloaded: transaction_queue.";
static std::string
configText(std::string const& txqLines)
{
return "[transaction_queue]\n" + txqLines +
"ledgers_in_queue = 2\n"
"minimum_queue_size = 2\n"
"\n"
"[ips]\n"
"r.ripple.com 51235\n";
}
static void
writeFile(std::filesystem::path const& path, std::string const& contents)
{
std::ofstream o(path.string(), std::ios::trunc);
o << contents;
}
static std::unique_ptr<Config>
configFromFile(std::filesystem::path const& path)
{
auto cfg = std::make_unique<Config>();
cfg->setup(path.string(), true, true, true);
test::setupConfigForUnitTests(*cfg);
return cfg;
}
void
testParams()
{
testcase("Params");
using namespace test::jtx;
Env env{*this};
auto const expectInvalid = [&](std::string const& params, std::string const& message) {
auto const result = env.rpc("json", "config_reload", params)[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == message);
};
expectInvalid("{}", "Missing field 'sections'.");
expectInvalid(R"({"sections": "transaction_queue"})", "Invalid field 'sections'.");
expectInvalid(R"({"sections": []})", "Invalid field 'sections'.");
expectInvalid(R"({"sections": [1]})", "Invalid field 'sections'.");
expectInvalid(R"({"sections": ["transaction_queue", "ips"]})", kIpsMessage);
// Valid params, but the server did not load a config file.
auto const result = env.rpc("config_reload", "transaction_queue")[jss::result];
BEAST_EXPECT(result[jss::error] == "notSupported");
BEAST_EXPECT(result[jss::error_code] == RpcNotSupported);
}
void
testReload()
{
testcase("Reload");
using namespace test::jtx;
detail::FileDirGuard const file(
*this,
"test_config_reload",
"xrpld.cfg",
configText("minimum_txn_in_ledger_standalone = 3\n"));
Env env{*this, configFromFile(file.file())};
auto const perLedger = [&] {
return env.app().getTxQ().getMetrics(*env.current()).txPerLedger;
};
BEAST_EXPECT(perLedger() == 3);
writeFile(file.file(), configText("minimum_txn_in_ledger_standalone = 7\n"));
// A request that includes a section that needs a restart applies
// nothing.
{
auto const result = env.rpc("config_reload", "transaction_queue", "ips")[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_code] == RpcInvalidParams);
BEAST_EXPECT(result[jss::error_message] == kIpsMessage);
BEAST_EXPECT(perLedger() == 3);
}
{
auto const result = env.rpc("config_reload", "transaction_queue")[jss::result];
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(result[jss::applied].size() == 1);
BEAST_EXPECT(result[jss::applied][0u] == "transaction_queue");
BEAST_EXPECT(perLedger() == 7);
}
// An invalid file changes nothing.
writeFile(
file.file(),
configText(
"minimum_txn_in_ledger_standalone = 9\n"
"maximum_txn_in_ledger = 1\n"));
{
auto const result = env.rpc("config_reload", "transaction_queue")[jss::result];
BEAST_EXPECT(result[jss::error] == "internal");
BEAST_EXPECT(
result[jss::error_message].asString().starts_with("Invalid config file: "));
BEAST_EXPECT(perLedger() == 7);
}
// A missing file changes nothing.
std::filesystem::remove(file.file());
{
auto const result = env.rpc("config_reload", "transaction_queue")[jss::result];
BEAST_EXPECT(result[jss::error] == "internal");
BEAST_EXPECT(perLedger() == 7);
}
// Put a file back so that the guard can remove it.
writeFile(file.file(), "");
}
public:
void
run() override
{
testParams();
testReload();
}
};
BEAST_DEFINE_TESTSUITE(ConfigReload, rpc, xrpl);
} // namespace xrpl
+51
View File
@@ -2578,6 +2578,57 @@ static RPCCallTestData const kRpcCallTestArray[] = {
})",
},
// config_reload
// -------------------------------------------------------------------
{
"config_reload: one section.",
__LINE__,
{"config_reload", "transaction_queue"},
RPCCallTestData::Exception::NoException,
R"({
"method" : "config_reload",
"params" : [
{
"api_version" : %API_VER%,
"sections" : [ "transaction_queue" ]
}
]
})",
},
{
"config_reload: two sections.",
__LINE__,
{"config_reload", "transaction_queue", "ips"},
RPCCallTestData::Exception::NoException,
R"({
"method" : "config_reload",
"params" : [
{
"api_version" : %API_VER%,
"sections" : [ "transaction_queue", "ips" ]
}
]
})",
},
{
"config_reload: too few arguments.",
__LINE__,
{
"config_reload",
},
RPCCallTestData::Exception::NoException,
R"({
"method" : "config_reload",
"params" : [
{
"error" : "badSyntax",
"error_code" : 1,
"error_message" : "Syntax error."
}
]
})",
},
// connect
// ---------------------------------------------------------------------
{
+1
View File
@@ -147,6 +147,7 @@ printHelp(po::options_description const& desc)
" can_delete [<ledgerid>|<ledgerhash>|now|always|never]\n"
" channel_authorize <private_key> <channel_id> <drops>\n"
" channel_verify <public_key> <channel_id> <drops> <signature>\n"
" config_reload <section> [<section> ...]\n"
" connect <ip> [<port>]\n"
" consensus_info\n"
" deposit_authorized <source_account> <destination_account> "
+24 -18
View File
@@ -366,6 +366,15 @@ public:
void
processClosedLedger(Application& app, ReadView const& view, bool timeLeap);
/**
* Replace the setup parameters while the server runs.
*
* @note The new limits apply at once. Transactions that are already in
* the queue stay there, even if the new limits are lower.
*/
void
reconfigure(Setup const& setup);
/**
* Return the next sequence that would go in the TxQ for an account.
*/
@@ -441,22 +450,22 @@ private:
/**
* Minimum value of txnsExpected.
*/
std::size_t const minimumTxnCount_;
std::size_t minimumTxnCount_ = 0;
/**
* Number of transactions per ledger that fee escalation "works
* towards".
*/
std::size_t const targetTxnCount_;
std::size_t targetTxnCount_ = 0;
/**
* Maximum value of txnsExpected
*/
std::optional<std::size_t> const maximumTxnCount_;
std::optional<std::size_t> maximumTxnCount_;
/**
* Number of transactions expected per ledger.
* One more than this value will be accepted
* before escalation kicks in.
*/
std::size_t txnsExpected_;
std::size_t txnsExpected_{0};
/**
* Recent history of transaction counts that
* exceed the targetTxnCount_
@@ -477,24 +486,21 @@ private:
* Constructor
*/
FeeMetrics(Setup const& setup, beast::Journal j)
: minimumTxnCount_(
setup.standAlone ? setup.minimumTxnInLedgerSA : setup.minimumTxnInLedger)
, targetTxnCount_(
setup.targetTxnInLedger < minimumTxnCount_ ? minimumTxnCount_
: setup.targetTxnInLedger)
, maximumTxnCount_([&] -> std::optional<std::size_t> {
if (!setup.maximumTxnInLedger)
return std::nullopt;
return *setup.maximumTxnInLedger < targetTxnCount_ ? targetTxnCount_
: *setup.maximumTxnInLedger;
}())
, txnsExpected_(minimumTxnCount_)
, recentTxnCounts_(setup.ledgersInQueue)
: recentTxnCounts_(setup.ledgersInQueue)
, escalationMultiplier_(setup.minimumEscalationMultiplier)
, j_(j)
{
// txnsExpected_ starts at 0, so this sets it to the minimum.
reconfigure(setup);
}
/**
* Apply new setup parameters. Keeps the current number of expected
* transactions, but moves it into the new limits.
*/
void
reconfigure(Setup const& setup);
/**
* Updates fee metrics based on the transactions in the ReadView
* for use in fee escalation calculations.
@@ -885,7 +891,7 @@ private:
/**
* Setup parameters used to control the behavior of the queue
*/
Setup const setup_;
Setup setup_;
/**
* Journal
*/
+32
View File
@@ -814,6 +814,23 @@ TxQApplyImpl::addTxToQueue()
//////////////////////////////////////////////////////////////////////////
void
TxQ::FeeMetrics::reconfigure(Setup const& setup)
{
minimumTxnCount_ = setup.standAlone ? setup.minimumTxnInLedgerSA : setup.minimumTxnInLedger;
targetTxnCount_ = std::max<std::size_t>(setup.targetTxnInLedger, minimumTxnCount_);
maximumTxnCount_.reset();
if (setup.maximumTxnInLedger)
maximumTxnCount_ = std::max<std::size_t>(*setup.maximumTxnInLedger, targetTxnCount_);
txnsExpected_ = std::max(txnsExpected_, minimumTxnCount_);
if (maximumTxnCount_)
txnsExpected_ = std::min(txnsExpected_, *maximumTxnCount_);
// Keep the most recent counts if the history gets shorter.
recentTxnCounts_.rset_capacity(setup.ledgersInQueue);
escalationMultiplier_ = std::max(escalationMultiplier_, setup.minimumEscalationMultiplier);
}
std::size_t
TxQ::FeeMetrics::update(
Application& app,
@@ -1693,6 +1710,21 @@ TxQ::processClosedLedger(Application& app, ReadView const& view, bool timeLeap)
}
}
void
TxQ::reconfigure(Setup const& setup)
{
std::scoped_lock const lock(mutex_);
setup_ = setup;
feeMetrics_.reconfigure(setup_);
// maxSize_ is not set until the first ledger closes.
if (maxSize_)
{
maxSize_ = std::max(
feeMetrics_.getSnapshot().txnsExpected * setup_.ledgersInQueue, setup_.queueSizeMin);
}
}
void
TxQ::rebuildQueue(OpenView& view)
{
+9
View File
@@ -416,6 +416,15 @@ public:
{
return j_;
}
/**
* Returns the path of the config file. Empty if no file was loaded.
*/
[[nodiscard]] std::filesystem::path const&
configFile() const
{
return configFile_;
}
};
FeeSetup
+1
View File
@@ -31,6 +31,7 @@ inline constexpr std::string_view kBookOffers{"book_offers"};
inline constexpr std::string_view kCanDelete{"can_delete"};
inline constexpr std::string_view kChannelAuthorize{"channel_authorize"};
inline constexpr std::string_view kChannelVerify{"channel_verify"};
inline constexpr std::string_view kConfigReload{"config_reload"};
inline constexpr std::string_view kConnect{"connect"};
inline constexpr std::string_view kConsensusInfo{"consensus_info"};
inline constexpr std::string_view kDepositAuthorized{"deposit_authorized"};
+6
View File
@@ -213,6 +213,12 @@ constexpr auto kHandlerArray = std::to_array<Handler>({
.role = Role::USER,
.condition = Condition::NoCondition,
},
{
.name = method::kConfigReload,
.valueMethod = Method::of<&byRef<&doConfigReload>>(),
.role = Role::ADMIN,
.condition = Condition::NoCondition,
},
{
.name = method::kConnect,
.valueMethod = Method::of<&byRef<&doConnect>>(),
+17
View File
@@ -477,6 +477,18 @@ private:
return jvRequest;
}
// config_reload <section> [<section> ...]
json::Value
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
parseConfigReload(json::Value const& jvParams)
{
json::Value jvRequest(json::ValueType::Object);
auto& sections = (jvRequest[jss::sections] = json::ValueType::Array);
for (json::UInt i = 0; i < jvParams.size(); ++i)
sections.append(jvParams[i].asString());
return jvRequest;
}
// connect <ip[:port]> [port]
json::Value
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
@@ -1412,6 +1424,11 @@ private:
.minParams = 4,
.maxParams = 4,
},
{
.name = rpc::method::kConfigReload,
.parse = Parse::of<&RPCParser::parseConfigReload>(),
.minParams = 1,
},
{
.name = rpc::method::kConnect,
.parse = Parse::of<&RPCParser::parseConnect>(),
+2
View File
@@ -23,6 +23,8 @@ doChannelAuthorize(rpc::JsonContext&);
json::Value
doChannelVerify(rpc::JsonContext&);
json::Value
doConfigReload(rpc::JsonContext&);
json::Value
doConnect(rpc::JsonContext&);
json::Value
doConsensusInfo(rpc::JsonContext&);
@@ -0,0 +1,82 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/Context.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/config/Constants.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <exception>
#include <set>
#include <string>
#include <system_error>
namespace xrpl {
// Reads the config file again and applies the requested sections. Each
// requested section must be one that can change while the server runs.
json::Value
doConfigReload(rpc::JsonContext& context)
{
auto const& params = context.params;
if (!params.isMember(jss::sections))
return rpc::missingFieldError(jss::sections);
auto const& requested = params[jss::sections];
if (!requested.isArray() || requested.size() == 0)
return rpc::invalidFieldError(jss::sections);
std::set<std::string> sections;
for (auto const& section : requested)
{
if (!section.isString())
return rpc::invalidFieldError(jss::sections);
auto const name = section.asString();
if (name != Sections::kTransactionQueue)
{
return rpc::makeParamError(
"Section '" + name +
"' in 'sections' cannot be reloaded without a restart. Sections that can be "
"reloaded: " +
Sections::kTransactionQueue + ".");
}
sections.insert(name);
}
Config const& running = context.app.config();
if (running.configFile().empty())
return rpc::makeError(RpcNotSupported, "The server did not load a config file.");
std::error_code ec;
auto const contents = getFileContents(ec, running.configFile());
if (ec)
return rpc::makeError(RpcInternal, "Unable to read the config file: " + ec.message());
// Check the full file before anything is applied.
Config updated;
updated.configDir = running.configDir;
TxQ::Setup txQSetup;
try
{
updated.loadFromString(contents);
txQSetup = setupTxQ(updated);
}
catch (std::exception const& e)
{
return rpc::makeError(RpcInternal, std::string("Invalid config file: ") + e.what());
}
txQSetup.standAlone = running.standalone();
context.app.getTxQ().reconfigure(txQSetup);
json::Value result(json::ValueType::Object);
auto& applied = (result[jss::applied] = json::ValueType::Array);
for (auto const& name : sections)
applied.append(name);
return result;
}
} // namespace xrpl