Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Holland
43df88a5ed allow rwdb to co-exist with pinned node_db 2026-10-14 19:52:00 +11:00
12 changed files with 81 additions and 207 deletions

View File

@@ -1271,79 +1271,6 @@ public:
BEAST_EXPECT(table->needValidatedLedger(257));
}
void
testIgnoredUnsupported()
{
testcase("ignore_amendments");
using namespace std::chrono_literals;
weeks constexpr w(1);
test::jtx::Env env{*this, makeConfig()};
static std::vector<AmendmentTable::FeatureInfo> const supported =
makeDefaultYes(yes_);
// Ignore every unsupported_ amendment except the first one.
BEAST_EXPECT(unsupported_.size() > 1);
std::vector<std::string> const ignoredNames(
unsupported_.begin() + 1, unsupported_.end());
auto table = make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
makeSection(ignoredNames));
for (auto const& a : ignoredNames)
BEAST_EXPECT(table->isSupported(amendmentId(a)));
BEAST_EXPECT(!table->isSupported(amendmentId(unsupported_[0])));
majorityAmendments_t const majority;
// Enabling only ignored amendments must not block.
std::set<uint256> enabled;
for (auto const& a : ignoredNames)
enabled.insert(amendmentId(a));
table->doValidatedLedger(1, enabled, majority);
BEAST_EXPECT(!table->hasUnsupportedEnabled());
BEAST_EXPECT(table->unsupportedAmendments().empty());
// An ignored amendment must never be voted for.
auto const desired = table->getDesired();
for (auto const& a : ignoredNames)
BEAST_EXPECT(
std::find(desired.begin(), desired.end(), amendmentId(a)) ==
desired.end());
// A non-ignored unsupported amendment still blocks.
enabled.insert(amendmentId(unsupported_[0]));
table->doValidatedLedger(2, enabled, majority);
BEAST_EXPECT(table->hasUnsupportedEnabled());
// Bad entries are rejected.
Section bad("ignore_amendments");
bad.append("NotARealFeatureName");
try
{
make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
bad);
fail("expected bad [ignore_amendments] entry to throw");
}
catch (std::runtime_error const&)
{
pass();
}
}
void
testFeature(FeatureBitset const& feat)
{
@@ -1367,7 +1294,6 @@ public:
testBadConfig();
testEnableVeto();
testHasUnsupported();
testIgnoredUnsupported();
testFeature(all - fixMajorityCalc);
testFeature(all);
}

View File

@@ -76,6 +76,10 @@ class SHAMapStore_test : public beast::unit_test::suite
return cfg;
}
// DatabasePinned with the in-memory RWDB backend as the rotating hot
// store and NuDB as the pinned store, over a persistent SQLite
// relational database and a file-backed state.db. This is the
// production shape for "rwdb + pinned_type".
static auto
pinnedPersistent(
std::unique_ptr<Config> cfg,
@@ -96,8 +100,9 @@ class SHAMapStore_test : public beast::unit_test::suite
}
// Online delete with DatabasePinned over in-memory backends. Unlike
// pinnedPersistent, the hot store keeps nodes (type=rwdb is the null
// node store), so ledgers closed in the test can be fetched. There is
// pinnedPersistent, the hot store is the process-shared "memory"
// backend and the pinned store is in-memory too, so ledgers closed in
// the test can be fetched without touching disk. There is
// no database_path, so every SHAMapStoreImp opens its own private
// state.db: a second instance built by a test does not see (or trip
// over) the rotation state of the Env's own.
@@ -936,15 +941,22 @@ public:
Config upperCfg;
Config nudbCfg;
Config emptyCfg;
Config pinnedCfg;
rwdbCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
upperCfg.section(ConfigSection::nodeDatabase()).set("type", "RWDB");
nudbCfg.section(ConfigSection::nodeDatabase()).set("type", "NuDB");
pinnedCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
pinnedCfg.section(ConfigSection::nodeDatabase())
.set("pinned_type", "NuDB");
// Evaluated per Config object, not latched process-wide.
BEAST_EXPECT(rwdbCfg.nullBackend());
BEAST_EXPECT(upperCfg.nullBackend());
BEAST_EXPECT(!nudbCfg.nullBackend());
BEAST_EXPECT(!emptyCfg.nullBackend());
// pinned_type turns type=rwdb into the rotating hot store under
// DatabasePinned, not the null node store.
BEAST_EXPECT(!pinnedCfg.nullBackend());
BEAST_EXPECT(rwdbCfg.nullBackend());
}

View File

@@ -1707,9 +1707,10 @@ r.ripple.com:51235
}
}
// Test 5: type=rwdb with pinned_type (should throw, even in
// standalone). The rwdb node store is tree-only, so pinned data
// would be accepted and then dropped.
// Test 5: type=rwdb with pinned_type (should succeed in both
// modes). pinned_type opts the rwdb store out of null mode: it
// becomes the in-memory rotating hot store under DatabasePinned,
// with no [node_db] path required.
for (bool const standalone : {false, true})
{
Config c;
@@ -1723,13 +1724,14 @@ r.ripple.com:51235
try
{
c.loadFromString(toLoad);
fail("Expected exception for pinned_type with type=rwdb");
BEAST_EXPECT(!c.nullBackend());
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("type=rwdb") !=
std::string::npos);
fail(
std::string("Should not throw for pinned_type with "
"type=rwdb: ") +
e.what());
}
}

View File

@@ -34,9 +34,9 @@ namespace test {
class DatabasePinned_test : public beast::unit_test::suite
{
// Build an Env whose nodestore is wired up as DatabasePinnedImp.
// The rotating store must persist (type=memory). type=rwdb is
// NullFactory and never builds DatabasePinnedImp. pinned_type=rwdb
// is allowed in standalone mode.
// The rotating store is the process-shared "memory" backend so the
// hot side keeps nodes across the test. pinned_type=rwdb is allowed
// in standalone mode.
static std::unique_ptr<Config>
pinnedEnvconfig(std::string const& nodePath)
{

View File

@@ -110,10 +110,10 @@ struct CatalogueEnvOpts
// envconfig with pinned_type set so catalogue_load passes the config guard.
//
// envconfig() selects [node_db] type=rwdb, which is the tree-only null node
// store: SHAMapStore never builds DatabasePinned on it, so catalogue_load
// would refuse to run. Use the in-memory "memory" store for the hot side
// instead. It is shared by path within the process, so each config gets its
// own path (SHAMapStore creates the directory; nothing is written to it).
// store unless pinned_type is also set. Use the in-memory "memory" store for
// the hot side so the Env's node store is the same shape as a persisting
// one. It is shared by path within the process, so each config gets its own
// path (SHAMapStore creates the directory; nothing is written to it).
// The pinned side uses rwdb, an in-memory persistent backend.
inline std::unique_ptr<Config>
catalogueEnvconfig(CatalogueEnvOpts const& opts = {})
@@ -1416,13 +1416,14 @@ public:
using namespace test::jtx;
using beast::severities::kDisabled;
// pinned_type is set, but [node_db] type=rwdb is the null node
// store, on which SHAMapStore never builds the pinned store.
// pinned_type is set, but online_delete is not, so SHAMapStore
// builds a plain node store rather than DatabasePinned. (Config
// validation rejects this combination from a config file; jtx
// sets sections directly and bypasses it.)
{
auto cfg = test::jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
nodeDb.set("pinned_type", "rwdb");
nodeDb.set("online_delete", "256");
Env env{*this, std::move(cfg), features, nullptr, kDisabled};
Json::Value params{Json::objectValue};

View File

@@ -1260,29 +1260,13 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
Section const& upVoted = config_->section(SECTION_AMENDMENTS);
Section const& ignored = config_->section(SECTION_IGNORE_AMENDMENTS);
// [ignore_amendments] exists to keep long-lived test networks syncable
// after an amendment they activated is removed from the codebase. It
// must never be usable on a production network.
if (!ignored.lines().empty() &&
(config_->NETWORK_ID == 0 || config_->NETWORK_ID == 21337))
{
JLOG(m_journal.fatal()) << "[" SECTION_IGNORE_AMENDMENTS
"] is not permitted on "
"network_id "
<< config_->NETWORK_ID;
return false;
}
m_amendmentTable = make_AmendmentTable(
*this,
config().AMENDMENT_MAJORITY_TIME,
supported,
upVoted,
downVoted,
logs_->journal("Amendments"),
ignored);
logs_->journal("Amendments"));
}
Pathfinder::initPathTable();

View File

@@ -211,8 +211,7 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored = Section{});
beast::Journal journal);
} // namespace ripple

View File

@@ -207,7 +207,11 @@ SHAMapStoreImp::SHAMapStoreImp(
std::to_string(config.LEDGER_HISTORY) + ")");
}
if (!isNullBackend_)
// Only on-disk rotating backends keep files under [node_db] path.
// The null node store has none, and neither does type=rwdb used
// as the rotating hot store under DatabasePinned (pinned_type
// set), which was never required to configure a path.
if (!isNullBackend_ && !boost::iequals(get(section, "type"), "rwdb"))
dbPaths();
}
}
@@ -425,6 +429,16 @@ SHAMapStoreImp::loadPinnedRanges()
// is not the one these ranges were loaded into.
constexpr std::uint32_t maxTailTrim = 65536;
// [relational_db] backend=rwdb keeps the SQL tables in memory, so the
// seq -> hash index behind hasLedgerData() starts empty on every run
// even when the pinned node store still holds every node. An interval
// the index cannot resolve is unreachable rather than evidence of a
// mismatched pinned store: drop it and have the operator re-run
// catalogue_load (node writes de-duplicate; only SQL is rebuilt).
auto const& rdb = app_.config().section(SECTION_RELATIONAL_DB);
bool const sqlPersistent =
rdb.empty() || !boost::iequals(get(rdb, "backend"), "rwdb");
RangeSet<std::uint32_t> verified;
for (auto const& interval : persistedRanges)
{
@@ -439,7 +453,18 @@ SHAMapStoreImp::loadPinnedRanges()
};
if (!hasLedgerData(lower))
Throw<std::runtime_error>(notPresent());
{
if (sqlPersistent)
Throw<std::runtime_error>(notPresent());
JLOG(journal_.warn())
<< "Dropping pinned interval " << lower << "-"
<< interval.upper()
<< ": [relational_db] backend=rwdb does not persist the "
"ledger index across restarts, so it cannot be served. "
"Re-run catalogue_load to restore it.";
continue;
}
std::uint32_t trimmed = 0;
while (upper > lower && !hasLedgerData(upper))
@@ -464,7 +489,7 @@ SHAMapStoreImp::loadPinnedRanges()
}
if (verified != persistedRanges)
state_db_.setPinnedRanges(to_string(verified));
setPinnedRanges(verified);
JLOG(journal_.info()) << "Loaded pinned ranges from database: "
<< to_string(verified);

View File

@@ -302,15 +302,6 @@ struct AmendmentState
/** Indicates an amendment that this server has code support for. */
bool supported = false;
/** Indicates an amendment listed in [ignore_amendments]. The server has
no code for it, but must not become amendment blocked if it is (or
becomes) enabled on the ledger. Such an amendment is reported as
`supported` internally so every blocking check (LedgerMaster,
NetworkOPs, Change) passes, but is never voted for and is shown as
unsupported + ignored over RPC.
*/
bool ignored = false;
/** The name of this amendment, possibly empty. */
std::string name;
@@ -485,8 +476,7 @@ public:
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored);
beast::Journal journal);
uint256
find(std::string const& name) const override;
@@ -553,8 +543,7 @@ AmendmentTableImpl::AmendmentTableImpl(
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored)
beast::Journal journal)
: lastUpdateSeq_(0)
, majorityTime_(majorityTime)
, unsupportedEnabled_(false)
@@ -597,59 +586,6 @@ AmendmentTableImpl::AmendmentTableImpl(
<< " voted by default if not enabled on the ledger.";
}
// Parse ignored amendments from config. Each line is either a registered
// feature name (e.g. "HookOnV2") or "<64 hex amendment ID> [name]". These
// are deliberately NOT persisted to wallet.db: removing the line from the
// config must restore normal amendment-blocking behaviour.
for (auto const& line : ignored.lines())
{
std::string const trimmed = boost::algorithm::trim_copy(line);
if (trimmed.empty())
continue;
std::vector<std::string> parts;
boost::algorithm::split(
parts,
trimmed,
boost::algorithm::is_space(),
boost::algorithm::token_compress_on);
uint256 id;
std::string name;
if (id.parseHex(parts[0]))
name = parts.size() > 1 ? parts[1] : std::string{};
else if (auto const f = getRegisteredFeature(parts[0]))
{
id = *f;
name = parts[0];
}
else
Throw<std::runtime_error>(
"Invalid entry '" + line + "' in [" + ignored.name() +
"]: expected a registered feature name or amendment ID");
AmendmentState& s = add(id, lock);
if (s.supported)
{
JLOG(j_.warn()) << "[" << ignored.name() << "] lists amendment "
<< id << " (" << s.name
<< ") which this server supports; entry has no "
"effect.";
continue;
}
if (!name.empty())
s.name = name;
s.supported = true;
s.ignored = true;
s.vote = AmendmentVote::obsolete;
JLOG(j_.warn()) << "Amendment " << id << " (" << s.name
<< ") is unsupported but listed in [" << ignored.name()
<< "]: this server will NOT become amendment blocked "
"if it is enabled on the ledger.";
}
hash_set<uint256> detect_conflict;
// Parse enabled amendments from config
for (std::pair<uint256, std::string> const& a : parseSection(enabled))
@@ -839,12 +775,7 @@ AmendmentTableImpl::enable(uint256 const& amendment)
s.enabled = true;
if (s.ignored)
{
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
<< s.name << ") is enabled on the ledger.";
}
else if (!s.supported)
if (!s.supported)
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated.";
@@ -1113,9 +1044,7 @@ AmendmentTableImpl::injectJson(
if (!fs.name.empty())
v[jss::name] = fs.name;
v[jss::supported] = fs.supported && !fs.ignored;
if (fs.ignored)
v["ignored"] = true;
v[jss::supported] = fs.supported;
if (!fs.enabled && isAdmin)
{
if (fs.vote == AmendmentVote::obsolete)
@@ -1184,11 +1113,10 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal,
Section const& ignored)
beast::Journal journal)
{
return std::make_unique<AmendmentTableImpl>(
app, majorityTime, supported, enabled, vetoed, journal, ignored);
app, majorityTime, supported, enabled, vetoed, journal);
}
} // namespace ripple

View File

@@ -387,10 +387,14 @@ public:
return isMem;
}
/** True when [node_db] type=rwdb.
/** True when [node_db] type=rwdb and no pinned_type is configured.
The app node store is the existing NullFactory (type=none).
Nodes stay alive through Ledger and SHAMap pointers.
With pinned_type set, type=rwdb keeps its older meaning: the
in-memory RWDB backend is the rotating hot store under
DatabasePinnedImp and the durable pinned store holds history.
The result is read from this configuration, so two
applications in one process do not share it.
*/
@@ -399,7 +403,8 @@ public:
{
auto const& node = section(ConfigSection::nodeDatabase());
return !node.empty() &&
boost::beast::iequals(get(node, "type"), "rwdb");
boost::beast::iequals(get(node, "type"), "rwdb") &&
!node.exists("pinned_type");
}
bool

View File

@@ -102,9 +102,6 @@ struct ConfigSection
#define SECTION_VALIDATORS "validators"
#define SECTION_VALIDATOR_TOKEN "validator_token"
#define SECTION_VETO_AMENDMENTS "veto_amendments"
// Testnet-only: amendments enabled on-ledger that this node should not treat
// as unsupported (i.e. should not become amendment blocked over).
#define SECTION_IGNORE_AMENDMENTS "ignore_amendments"
#define SECTION_WORKERS "workers"
} // namespace ripple

View File

@@ -1087,15 +1087,10 @@ Config::loadFromString(std::string const& fileContents)
"type must be specified when using pinned_type");
}
// type=rwdb is the tree-only null node store. SHAMapStore never
// builds DatabasePinned for it, so pinned (and catalogue) data
// would be accepted and then silently dropped.
if (nullBackend())
{
Throw<std::runtime_error>(
"pinned_type is not supported with [node_db] type=rwdb "
"(the rwdb node store does not persist nodes)");
}
// type=rwdb with pinned_type is not the null node store (see
// Config::nullBackend): SHAMapStore builds DatabasePinned with the
// in-memory RWDB backend as the rotating hot store, so pinned data
// lands in the durable pinned backend as with any other type.
// Ensure pinned_path is specified for filesystem-backed types.
// In-memory backends (rwdb) don't need a path.