feat(runtime-config): support startup message filters

This commit is contained in:
Nicholas Dudfield
2026-05-22 14:50:59 +08:00
parent 24d6dea1a2
commit 331e1606a3
4 changed files with 344 additions and 58 deletions

View File

@@ -22,10 +22,37 @@
#include <xrpld/overlay/detail/TrafficCount.h>
#include <xrpl/protocol/jss.h>
#include <cstdlib>
#include <optional>
#include <string>
namespace ripple {
class RuntimeConfig_test : public beast::unit_test::suite
{
class EnvVarGuard
{
public:
EnvVarGuard(char const* name, char const* value) : name_(name)
{
if (auto const* old = std::getenv(name))
old_ = old;
setenv(name, value, 1);
}
~EnvVarGuard()
{
if (old_)
setenv(name_, old_->c_str(), 1);
else
unsetenv(name_);
}
private:
char const* name_;
std::optional<std::string> old_;
};
// Helper to call runtime_config RPC with JSON params
Json::Value
runtimeConfig(test::jtx::Env& env, Json::Value const& params)
@@ -265,6 +292,71 @@ class RuntimeConfig_test : public beast::unit_test::suite
BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::base));
}
void
testMessageTypeAliases()
{
testcase("Message type aliases expand to multiple categories");
using namespace test::jtx;
Env env{*this};
Json::Value params;
params["set"] = Json::objectValue;
params["set"]["*"] = Json::objectValue;
params["set"]["*"]["send_delay_ms"] = 100;
params["set"]["*"]["message_types"] = Json::arrayValue;
params["set"]["*"]["message_types"].append("candidate_set_fetch");
auto result = runtimeConfig(env, params);
auto const& global = result["configs"]["*"];
BEAST_EXPECT(global.isMember("message_types"));
BEAST_EXPECT(global["message_types"].size() == 4);
auto cfg = env.app().getRuntimeConfig().getConfig("*");
if (!BEAST_EXPECT(cfg.has_value()))
return;
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_get));
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_share));
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_get));
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_share));
BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::proposal));
}
void
testEnvMessageTypeFilter()
{
testcase("XAHAU_RUNTIME_CONFIG parses message_types");
EnvVarGuard guard{
"XAHAU_RUNTIME_CONFIG",
R"({"*":{"send_delay_ms":100,"message_types":["candidate_set_fetch"]}})"};
RuntimeConfig rc;
auto cfg = rc.getConfig("*");
if (!BEAST_EXPECT(cfg.has_value()))
return;
BEAST_EXPECT(rc.active());
BEAST_EXPECT(cfg->sendDelayMs == 100);
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::gl_tsc_get));
BEAST_EXPECT(cfg->appliesTo(TrafficCount::category::ld_tsc_share));
BEAST_EXPECT(!cfg->appliesTo(TrafficCount::category::proposal));
}
void
testInvalidEnvMessageType()
{
testcase("Invalid XAHAU_RUNTIME_CONFIG message_types are ignored");
EnvVarGuard guard{
"XAHAU_RUNTIME_CONFIG",
R"({"*":{"send_delay_ms":100,"message_types":["not_a_category"]}})"};
RuntimeConfig rc;
BEAST_EXPECT(!rc.active());
BEAST_EXPECT(!rc.getConfig("*").has_value());
}
void
testMessageTypeFilterEmpty()
{
@@ -313,6 +405,25 @@ class RuntimeConfig_test : public beast::unit_test::suite
BEAST_EXPECT(!env.app().getRuntimeConfig().active());
}
void
testInvalidMessageTypesShape()
{
testcase("Non-array message_types returns error");
using namespace test::jtx;
Env env{*this};
Json::Value params;
params["set"] = Json::objectValue;
params["set"]["*"] = Json::objectValue;
params["set"]["*"]["send_delay_ms"] = 100;
params["set"]["*"]["message_types"] = "proposal";
auto result = runtimeConfig(env, params);
BEAST_EXPECT(result.isMember("error"));
BEAST_EXPECT(result["error"].asString() == "invalidParams");
BEAST_EXPECT(!env.app().getRuntimeConfig().active());
}
void
testDropPctClamping()
{
@@ -515,8 +626,12 @@ public:
testClearAll();
testPerPeerWithoutGlobal();
testMessageTypeFilter();
testMessageTypeAliases();
testEnvMessageTypeFilter();
testInvalidEnvMessageType();
testMessageTypeFilterEmpty();
testInvalidMessageType();
testInvalidMessageTypesShape();
testDropPctClamping();
testRngClaimDropPct();
testRngClaimDropPctClamping();

View File

@@ -26,6 +26,7 @@
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace ripple {
@@ -177,6 +178,22 @@ private:
std::unordered_map<std::string, ConfigVals> merged_;
};
/** Expand runtime-config message type names into TrafficCount categories.
Names are intentionally string-based so test tools can target overlay
traffic without depending on enum values. Aliases may expand to several
categories, for example candidate-set fetch covers the TMGetLedger request
and TMLedgerData reply categories used by tx-set and sidecar acquisition.
*/
std::optional<std::set<std::size_t>>
runtimeConfigMessageCategoriesFromNames(
std::vector<std::string> const& names,
std::string& error);
/** Human-readable name for one runtime-config message category. */
std::string
runtimeConfigMessageCategoryName(std::size_t category);
} // namespace ripple
#endif

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <xrpld/app/misc/RuntimeConfig.h>
#include <xrpld/overlay/detail/TrafficCount.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_value.h>
@@ -25,12 +26,148 @@
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <exception>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <vector>
namespace ripple {
namespace {
using CategorySet = std::set<std::size_t>;
struct CategoryAlias
{
char const* name;
std::vector<std::size_t> categories;
};
std::vector<CategoryAlias> const&
categoryAliases()
{
using C = TrafficCount::category;
static std::vector<CategoryAlias> const aliases = {
{"base", {C::base}},
{"cluster", {C::cluster}},
{"overlay", {C::overlay}},
{"proposal", {C::proposal}},
{"validation", {C::validation}},
{"transaction", {C::transaction}},
{"manifests", {C::manifests}},
{"validator_list", {C::validatorlist}},
{"validatorlist", {C::validatorlist}},
{"have_set", {C::get_set, C::share_set}},
{"set_get", {C::get_set}},
{"set_share", {C::share_set}},
{"candidate_set_fetch",
{C::gl_tsc_get, C::gl_tsc_share, C::ld_tsc_get, C::ld_tsc_share}},
{"candidate_set_request", {C::gl_tsc_get}},
{"candidate_set_reply", {C::ld_tsc_share}},
{"ledger_data",
{C::ld_tsc_get,
C::ld_tsc_share,
C::ld_txn_get,
C::ld_txn_share,
C::ld_asn_get,
C::ld_asn_share,
C::ld_get,
C::ld_share}},
{"ledger_data_tsc_get", {C::ld_tsc_get}},
{"ledger_data_tsc_share", {C::ld_tsc_share}},
{"ledger_data_txn_get", {C::ld_txn_get}},
{"ledger_data_txn_share", {C::ld_txn_share}},
{"ledger_data_asn_get", {C::ld_asn_get}},
{"ledger_data_asn_share", {C::ld_asn_share}},
{"ledger_data_get", {C::ld_get}},
{"ledger_data_share", {C::ld_share}},
{"get_ledger",
{C::gl_tsc_get,
C::gl_tsc_share,
C::gl_txn_get,
C::gl_txn_share,
C::gl_asn_get,
C::gl_asn_share,
C::gl_get,
C::gl_share}},
{"get_ledger_tsc_get", {C::gl_tsc_get}},
{"get_ledger_tsc_share", {C::gl_tsc_share}},
{"get_ledger_txn_get", {C::gl_txn_get}},
{"get_ledger_txn_share", {C::gl_txn_share}},
{"get_ledger_asn_get", {C::gl_asn_get}},
{"get_ledger_asn_share", {C::gl_asn_share}},
{"get_ledger_get", {C::gl_get}},
{"get_ledger_share", {C::gl_share}},
{"get_object",
{C::share_hash_ledger,
C::get_hash_ledger,
C::share_hash_tx,
C::get_hash_tx,
C::share_hash_txnode,
C::get_hash_txnode,
C::share_hash_asnode,
C::get_hash_asnode,
C::share_cas_object,
C::get_cas_object,
C::share_fetch_pack,
C::get_fetch_pack,
C::get_transactions,
C::share_hash,
C::get_hash}},
{"get_object_fetch_pack", {C::share_fetch_pack, C::get_fetch_pack}},
{"get_object_fetch_pack_get", {C::get_fetch_pack}},
{"get_object_fetch_pack_share", {C::share_fetch_pack}},
{"get_object_get", {C::get_hash}},
{"get_object_share", {C::share_hash}},
{"get_object_transactions", {C::get_transactions}},
{"proof_path", {C::proof_path_request, C::proof_path_response}},
{"proof_path_request", {C::proof_path_request}},
{"proof_path_response", {C::proof_path_response}},
{"replay_delta", {C::replay_delta_request, C::replay_delta_response}},
{"replay_delta_request", {C::replay_delta_request}},
{"replay_delta_response", {C::replay_delta_response}},
{"have_transactions", {C::have_transactions}},
{"requested_transactions", {C::requested_transactions}},
};
return aliases;
}
std::optional<CategorySet>
categoriesForName(std::string const& name)
{
for (auto const& alias : categoryAliases())
{
if (name == alias.name)
return CategorySet{
alias.categories.begin(), alias.categories.end()};
}
if (!name.empty() &&
std::all_of(name.begin(), name.end(), [](unsigned char c) {
return std::isdigit(c);
}))
{
try
{
auto const cat = static_cast<std::size_t>(std::stoull(name));
if (cat <= TrafficCount::category::unknown)
return CategorySet{cat};
}
catch (std::exception const&)
{
}
}
return std::nullopt;
}
std::optional<bool>
parseBoolEnv(char const* env)
{
@@ -51,7 +188,7 @@ parseBoolEnv(char const* env)
return std::nullopt;
}
ConfigVals
std::optional<ConfigVals>
parseConfigVals(Json::Value const& v)
{
ConfigVals cfg;
@@ -73,10 +210,56 @@ parseConfigVals(Json::Value const& v)
cfg.rngPollMs = std::max(50, v["rng_poll_ms"].asInt());
if (v.isMember("no_export_sig"))
cfg.noExportSig = v["no_export_sig"].asBool();
if (v.isMember("message_types"))
{
if (!v["message_types"].isArray())
return std::nullopt;
std::vector<std::string> names;
for (auto const& mt : v["message_types"])
names.push_back(mt.asString());
std::string error;
auto cats = runtimeConfigMessageCategoriesFromNames(names, error);
if (!cats)
return std::nullopt;
cfg.messageCategories = *cats;
}
return cfg;
}
} // namespace
std::optional<std::set<std::size_t>>
runtimeConfigMessageCategoriesFromNames(
std::vector<std::string> const& names,
std::string& error)
{
CategorySet result;
for (auto const& name : names)
{
auto cats = categoriesForName(name);
if (!cats)
{
error = "Unknown message_type: " + name;
return std::nullopt;
}
result.insert(cats->begin(), cats->end());
}
return result;
}
std::string
runtimeConfigMessageCategoryName(std::size_t category)
{
for (auto const& alias : categoryAliases())
{
if (alias.categories.size() == 1 &&
alias.categories.front() == category)
return alias.name;
}
return std::to_string(category);
}
RuntimeConfig::RuntimeConfig()
{
// XAHAU_RUNTIME_CONFIG takes precedence (full JSON config)
@@ -88,7 +271,10 @@ RuntimeConfig::RuntimeConfig()
{
std::unique_lock lock(mutex_);
for (auto const& target : root.getMemberNames())
configs_[target] = parseConfigVals(root[target]);
{
if (auto cfg = parseConfigVals(root[target]))
configs_[target] = *cfg;
}
rebuildMerged();
updateActive();
}

View File

@@ -19,49 +19,12 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/RuntimeConfig.h>
#include <xrpld/overlay/detail/TrafficCount.h>
#include <xrpld/rpc/Context.h>
#include <vector>
namespace ripple {
namespace {
// Map user-friendly names to TrafficCount::category values.
// Only the commonly useful categories are exposed; extend as needed.
std::optional<std::size_t>
categoryFromName(std::string const& name)
{
static std::unordered_map<std::string, std::size_t> const map = {
{"proposal", TrafficCount::category::proposal},
{"validation", TrafficCount::category::validation},
{"transaction", TrafficCount::category::transaction},
{"manifests", TrafficCount::category::manifests},
{"ledger_data", TrafficCount::category::ld_share},
{"get_ledger", TrafficCount::category::gl_get},
};
auto it = map.find(name);
if (it != map.end())
return it->second;
return std::nullopt;
}
std::string
categoryToName(std::size_t cat)
{
static std::unordered_map<std::size_t, std::string> const map = {
{TrafficCount::category::proposal, "proposal"},
{TrafficCount::category::validation, "validation"},
{TrafficCount::category::transaction, "transaction"},
{TrafficCount::category::manifests, "manifests"},
{TrafficCount::category::ld_share, "ledger_data"},
{TrafficCount::category::gl_get, "get_ledger"},
};
auto it = map.find(cat);
if (it != map.end())
return it->second;
return std::to_string(cat);
}
} // namespace
Json::Value
doRuntimeConfig(RPC::JsonContext& context)
{
@@ -111,24 +74,29 @@ doRuntimeConfig(RPC::JsonContext& context)
if (v.isMember("message_types"))
{
auto const& mts = v["message_types"];
cfg.messageCategories.emplace(); // set to empty = "all"
if (mts.isArray())
if (!mts.isArray())
{
for (auto const& mt : mts)
{
auto const name = mt.asString();
auto cat = categoryFromName(name);
if (!cat)
{
Json::Value err{Json::objectValue};
err["error"] = "invalidParams";
err["error_message"] =
"Unknown message_type: " + name;
return err;
}
cfg.messageCategories->insert(*cat);
}
Json::Value err{Json::objectValue};
err["error"] = "invalidParams";
err["error_message"] = "message_types must be an array";
return err;
}
std::vector<std::string> names;
for (auto const& mt : mts)
names.push_back(mt.asString());
std::string error;
auto cats =
runtimeConfigMessageCategoriesFromNames(names, error);
if (!cats)
{
Json::Value err{Json::objectValue};
err["error"] = "invalidParams";
err["error_message"] = error;
return err;
}
cfg.messageCategories = *cats;
}
rc.setConfig(target, cfg);
}
@@ -175,7 +143,7 @@ doRuntimeConfig(RPC::JsonContext& context)
{
Json::Value types{Json::arrayValue};
for (auto cat : *cfg.messageCategories)
types.append(categoryToName(cat));
types.append(runtimeConfigMessageCategoryName(cat));
entry["message_types"] = types;
}
configs[target] = entry;