#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test { class SHAMapStore_test : public beast::unit_test::Suite { static auto const kDELETE_INTERVAL = 8; static auto onlineDelete(std::unique_ptr cfg) { using namespace jtx; return online_delete(std::move(cfg), kDELETE_INTERVAL); } static auto advisoryDelete(std::unique_ptr cfg) { cfg = onlineDelete(std::move(cfg)); cfg->section(ConfigSection::nodeDatabase()).set("advisory_delete", "1"); return cfg; } static bool goodLedger(jtx::Env& env, json::Value const& json, std::string ledgerID, bool checkDB = false) { auto good = json.isMember(jss::result) && !RPC::containsError(json[jss::result]) && json[jss::result][jss::ledger][jss::ledger_index] == ledgerID; if (!good || !checkDB) return good; auto const seq = json[jss::result][jss::ledger_index].asUInt(); std::optional outInfo = env.app().getRelationalDatabase().getLedgerInfoByIndex(seq); if (!outInfo) return false; LedgerHeader const& info = outInfo.value(); std::string const outHash = to_string(info.hash); LedgerIndex const outSeq = info.seq; std::string const outParentHash = to_string(info.parentHash); std::string const outDrops = to_string(info.drops); std::uint64_t const outCloseTime = info.closeTime.time_since_epoch().count(); std::uint64_t const outParentCloseTime = info.parentCloseTime.time_since_epoch().count(); std::uint64_t const outCloseTimeResolution = info.closeTimeResolution.count(); std::uint64_t const outCloseFlags = info.closeFlags; std::string const outAccountHash = to_string(info.accountHash); std::string const outTxHash = to_string(info.txHash); auto const& ledger = json[jss::result][jss::ledger]; return outHash == ledger[jss::ledger_hash].asString() && outSeq == seq && outParentHash == ledger[jss::parent_hash].asString() && outDrops == ledger[jss::total_coins].asString() && outCloseTime == ledger[jss::close_time].asUInt() && outParentCloseTime == ledger[jss::parent_close_time].asUInt() && outCloseTimeResolution == ledger[jss::close_time_resolution].asUInt() && outCloseFlags == ledger[jss::close_flags].asUInt() && outAccountHash == ledger[jss::account_hash].asString() && outTxHash == ledger[jss::transaction_hash].asString(); } static bool bad(json::Value const& json, ErrorCodeI error = RpcLgrNotFound) { return json.isMember(jss::result) && RPC::containsError(json[jss::result]) && json[jss::result][jss::error_code] == error; } std::string getHash(json::Value const& json) { BEAST_EXPECT( json.isMember(jss::result) && json[jss::result].isMember(jss::ledger) && json[jss::result][jss::ledger].isMember(jss::ledger_hash) && json[jss::result][jss::ledger][jss::ledger_hash].isString()); return json[jss::result][jss::ledger][jss::ledger_hash].asString(); } void ledgerCheck(jtx::Env& env, int const rows, int const first) { auto const [actualRows, actualFirst, actualLast] = env.app().getRelationalDatabase().getLedgerCountMinMax(); BEAST_EXPECT(actualRows == rows); BEAST_EXPECT(actualFirst == first); BEAST_EXPECT(actualLast == first + rows - 1); } void transactionCheck(jtx::Env& env, int const rows) { BEAST_EXPECT(env.app().getRelationalDatabase().getTransactionCount() == rows); } void accountTransactionCheck(jtx::Env& env, int const rows) { BEAST_EXPECT(env.app().getRelationalDatabase().getAccountTransactionCount() == rows); } int waitForReady(jtx::Env& env) { using namespace std::chrono_literals; auto& store = env.app().getSHAMapStore(); int ledgerSeq = 3; store.rendezvous(); BEAST_EXPECT(!store.getLastRotated()); env.close(); store.rendezvous(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++))); BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1); return ledgerSeq; } public: void testClear() { using namespace std::chrono_literals; testcase("clearPrior"); using namespace jtx; Env env(*this, envconfig(onlineDelete)); auto& store = env.app().getSHAMapStore(); env.fund(XRP(10000), noripple("alice")); ledgerCheck(env, 1, 2); transactionCheck(env, 0); accountTransactionCheck(env, 0); std::map ledgers; auto ledgerTmp = env.rpc("ledger", "0"); BEAST_EXPECT(bad(ledgerTmp)); ledgers.emplace(1, env.rpc("ledger", "1")); BEAST_EXPECT(goodLedger(env, ledgers[1], "1")); ledgers.emplace(2, env.rpc("ledger", "2")); BEAST_EXPECT(goodLedger(env, ledgers[2], "2")); ledgerTmp = env.rpc("ledger", "current"); BEAST_EXPECT(goodLedger(env, ledgerTmp, "3")); ledgerTmp = env.rpc("ledger", "4"); BEAST_EXPECT(bad(ledgerTmp)); ledgerTmp = env.rpc("ledger", "100"); BEAST_EXPECT(bad(ledgerTmp)); auto const firstSeq = waitForReady(env); auto lastRotated = firstSeq - 1; for (auto i = firstSeq + 1; i < kDELETE_INTERVAL + firstSeq; ++i) { env.fund(XRP(10000), noripple("test" + std::to_string(i))); env.close(); ledgerTmp = env.rpc("ledger", "current"); BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i))); } BEAST_EXPECT(store.getLastRotated() == lastRotated); for (auto i = 3; i < kDELETE_INTERVAL + lastRotated; ++i) { ledgers.emplace(i, env.rpc("ledger", std::to_string(i))); BEAST_EXPECT( goodLedger(env, ledgers[i], std::to_string(i), true) && !getHash(ledgers[i]).empty()); } ledgerCheck(env, kDELETE_INTERVAL + 1, 2); transactionCheck(env, kDELETE_INTERVAL); accountTransactionCheck(env, 2 * kDELETE_INTERVAL); { // Closing one more ledger triggers a rotate env.close(); auto ledger = env.rpc("ledger", "current"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(kDELETE_INTERVAL + 4))); } store.rendezvous(); BEAST_EXPECT(store.getLastRotated() == kDELETE_INTERVAL + 3); lastRotated = store.getLastRotated(); BEAST_EXPECT(lastRotated == 11); // That took care of the fake hashes ledgerCheck(env, kDELETE_INTERVAL + 1, 3); transactionCheck(env, kDELETE_INTERVAL); accountTransactionCheck(env, 2 * kDELETE_INTERVAL); // The last iteration of this loop should trigger a rotate for (auto i = lastRotated - 1; i < lastRotated + kDELETE_INTERVAL - 1; ++i) { env.close(); ledgerTmp = env.rpc("ledger", "current"); BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i + 3))); ledgers.emplace(i, env.rpc("ledger", std::to_string(i))); BEAST_EXPECT( store.getLastRotated() == lastRotated || i == lastRotated + kDELETE_INTERVAL - 2); BEAST_EXPECT( goodLedger(env, ledgers[i], std::to_string(i), true) && !getHash(ledgers[i]).empty()); } store.rendezvous(); BEAST_EXPECT(store.getLastRotated() == kDELETE_INTERVAL + lastRotated); ledgerCheck(env, kDELETE_INTERVAL + 1, lastRotated); transactionCheck(env, 0); accountTransactionCheck(env, 0); } void testAutomatic() { testcase("automatic online_delete"); using namespace jtx; using namespace std::chrono_literals; Env env(*this, envconfig(onlineDelete)); auto& store = env.app().getSHAMapStore(); auto ledgerSeq = waitForReady(env); auto lastRotated = ledgerSeq - 1; BEAST_EXPECT(store.getLastRotated() == lastRotated); BEAST_EXPECT(lastRotated != 2); // Because advisory_delete is unset, // "can_delete" is disabled. auto const canDelete = env.rpc("can_delete"); BEAST_EXPECT(bad(canDelete, RpcNotEnabled)); // Close ledgers without triggering a rotate for (; ledgerSeq < lastRotated + kDELETE_INTERVAL; ++ledgerSeq) { env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); // The database will always have back to ledger 2, // regardless of lastRotated. ledgerCheck(env, ledgerSeq - 2, 2); BEAST_EXPECT(lastRotated == store.getLastRotated()); { // Closing one more ledger triggers a rotate env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - lastRotated, lastRotated); BEAST_EXPECT(lastRotated != store.getLastRotated()); lastRotated = store.getLastRotated(); // Close enough ledgers to trigger another rotate for (; ledgerSeq < lastRotated + kDELETE_INTERVAL + 1; ++ledgerSeq) { env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); ledgerCheck(env, kDELETE_INTERVAL + 1, lastRotated); BEAST_EXPECT(lastRotated != store.getLastRotated()); } void testCanDelete() { testcase("online_delete with advisory_delete"); using namespace jtx; using namespace std::chrono_literals; // Same config with advisory_delete enabled Env env(*this, envconfig(advisoryDelete)); auto& store = env.app().getSHAMapStore(); auto ledgerSeq = waitForReady(env); auto lastRotated = ledgerSeq - 1; BEAST_EXPECT(store.getLastRotated() == lastRotated); BEAST_EXPECT(lastRotated != 2); auto canDelete = env.rpc("can_delete"); BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); canDelete = env.rpc("can_delete", "never"); BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); auto const firstBatch = kDELETE_INTERVAL + ledgerSeq; for (; ledgerSeq < firstBatch; ++ledgerSeq) { env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - 2, 2); BEAST_EXPECT(lastRotated == store.getLastRotated()); // This does not kick off a cleanup canDelete = env.rpc("can_delete", std::to_string(ledgerSeq + (kDELETE_INTERVAL / 2))); BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + (kDELETE_INTERVAL / 2)); store.rendezvous(); ledgerCheck(env, ledgerSeq - 2, 2); BEAST_EXPECT(store.getLastRotated() == lastRotated); { // This kicks off a cleanup, but it stays small. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - lastRotated, lastRotated); BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1); lastRotated = ledgerSeq - 1; for (; ledgerSeq < lastRotated + kDELETE_INTERVAL; ++ledgerSeq) { // No cleanups in this loop. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); BEAST_EXPECT(store.getLastRotated() == lastRotated); { // This kicks off another cleanup. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - firstBatch, firstBatch); BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1); lastRotated = ledgerSeq - 1; // This does not kick off a cleanup canDelete = env.rpc("can_delete", "always"); BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); BEAST_EXPECT( canDelete[jss::result][jss::can_delete] == std::numeric_limits::max()); for (; ledgerSeq < lastRotated + kDELETE_INTERVAL; ++ledgerSeq) { // No cleanups in this loop. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); BEAST_EXPECT(store.getLastRotated() == lastRotated); { // This kicks off another cleanup. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - lastRotated, lastRotated); BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1); lastRotated = ledgerSeq - 1; // This does not kick off a cleanup canDelete = env.rpc("can_delete", "now"); BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq - 1); for (; ledgerSeq < lastRotated + kDELETE_INTERVAL; ++ledgerSeq) { // No cleanups in this loop. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true)); } store.rendezvous(); BEAST_EXPECT(store.getLastRotated() == lastRotated); { // This kicks off another cleanup. env.close(); auto ledger = env.rpc("ledger", "validated"); BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true)); } store.rendezvous(); ledgerCheck(env, ledgerSeq - lastRotated, lastRotated); BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1); lastRotated = ledgerSeq - 1; } std::unique_ptr makeBackendRotating(jtx::Env& env, NodeStoreScheduler& scheduler, std::string path) { Section section{env.app().config().section(ConfigSection::nodeDatabase())}; boost::filesystem::path newPath; if (!BEAST_EXPECT(path.size())) return {}; newPath = path; section.set("path", newPath.string()); auto backend{NodeStore::Manager::instance().makeBackend( section, megabytes(env.app().config().getValueFor(SizedItem::BurstSize, std::nullopt)), scheduler, env.app().getJournal("NodeStoreTest"))}; backend->open(); return backend; } void testRotate() { // The only purpose of this test is to ensure that if something that // should never happen happens, we don't get a deadlock. testcase("rotate with lock contention"); using namespace jtx; Env env(*this, envconfig(onlineDelete)); ///////////////////////////////////////////////////////////// // Create NodeStore with two backends to allow online deletion of data. // Normally, SHAMapStoreImp handles all these details. NodeStoreScheduler scheduler(env.app().getJobQueue()); std::string const writableDb = "write"; std::string const archiveDb = "archive"; auto writableBackend = makeBackendRotating(env, scheduler, writableDb); auto archiveBackend = makeBackendRotating(env, scheduler, archiveDb); constexpr int kREAD_THREADS = 4; auto nscfg = env.app().config().section(ConfigSection::nodeDatabase()); auto dbr = std::make_unique( scheduler, kREAD_THREADS, std::move(writableBackend), std::move(archiveBackend), nscfg, env.app().getJournal("NodeStoreTest")); ///////////////////////////////////////////////////////////// // Check basic functionality using namespace std::chrono_literals; std::atomic threadNum = 0; { auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum)); auto const cb = [&](std::string const& writableName, std::string const& archiveName) { BEAST_EXPECT(writableName == "1"); BEAST_EXPECT(archiveName == "write"); // Ensure that dbr functions can be called from within the // callback BEAST_EXPECT(dbr->getName() == "1"); }; dbr->rotate(std::move(newBackend), cb); } BEAST_EXPECT(threadNum == 1); BEAST_EXPECT(dbr->getName() == "1"); ///////////////////////////////////////////////////////////// // Do something stupid. Try to re-enter rotate from inside the callback. { auto const cb = [&](std::string const& writableName, std::string const& archiveName) { BEAST_EXPECT(writableName == "3"); BEAST_EXPECT(archiveName == "2"); // Ensure that dbr functions can be called from within the // callback BEAST_EXPECT(dbr->getName() == "3"); }; auto const cbReentrant = [&](std::string const& writableName, std::string const& archiveName) { BEAST_EXPECT(writableName == "2"); BEAST_EXPECT(archiveName == "1"); auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum)); // Reminder: doing this is stupid and should never happen dbr->rotate(std::move(newBackend), cb); }; auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum)); dbr->rotate(std::move(newBackend), cbReentrant); } BEAST_EXPECT(threadNum == 3); BEAST_EXPECT(dbr->getName() == "3"); } void testLedgerGaps() { // Note that this test is intentionally very similar to // LedgerMaster_test::testCompleteLedgerRange, but has a different // focus. testcase("Wait for ledger gaps to fill in"); using namespace test::jtx; Env env{*this, envconfig(onlineDelete)}; auto failureMessage = [&](char const* label, auto expected, auto actual) { std::stringstream ss; ss << label << ": Expected: " << expected << ", Got: " << actual; return ss.str(); }; auto const alice = Account("alice"); env.fund(XRP(1000), alice); env.close(); auto& lm = env.app().getLedgerMaster(); LedgerIndex minSeq = 2; LedgerIndex maxSeq = env.closed()->header().seq; auto& store = env.app().getSHAMapStore(); store.rendezvous(); LedgerIndex lastRotated = store.getLastRotated(); BEAST_EXPECTS(maxSeq == 3, std::to_string(maxSeq)); BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers()); BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated)); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2); // Close enough ledgers to rotate a few times while (maxSeq < 20) { for (int t = 0; t < 3; ++t) { env(noop(alice)); } env.close(); store.rendezvous(); ++maxSeq; if (maxSeq + 1 == lastRotated + kDELETE_INTERVAL) { using namespace std::chrono_literals; // The next ledger will trigger a rotation. Delete the // current ledger from LedgerMaster. std::this_thread::sleep_for(100ms); LedgerIndex const deleteSeq = maxSeq; while (!lm.haveLedger(deleteSeq)) { std::this_thread::sleep_for(100ms); } lm.clearLedger(deleteSeq); auto expectedRange = [](auto minSeq, auto deleteSeq, auto maxSeq) { std::stringstream expectedRange; expectedRange << minSeq << "-" << (deleteSeq - 1); if (deleteSeq + 1 == maxSeq) { expectedRange << "," << maxSeq; } else if (deleteSeq < maxSeq) { expectedRange << "," << (deleteSeq + 1) << "-" << maxSeq; } return expectedRange.str(); }; BEAST_EXPECTS( lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq), failureMessage( "Complete ledgers", expectedRange(minSeq, deleteSeq, maxSeq), lm.getCompleteLedgers())); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 1); // Close another ledger, which will trigger a rotation, but the // rotation will be stuck until the missing ledger is filled in. env.close(); // DO NOT CALL rendezvous()! You'll end up with a deadlock. ++maxSeq; // Nothing has changed BEAST_EXPECTS( store.getLastRotated() == lastRotated, failureMessage("lastRotated", lastRotated, store.getLastRotated())); BEAST_EXPECTS( lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq), failureMessage( "Complete ledgers", expectedRange(minSeq, deleteSeq, maxSeq), lm.getCompleteLedgers())); // Close 5 more ledgers, waiting one second in between to // simulate the ledger making progress while online delete waits // for the missing ledger to be filled in. // This ensures the healthWait check has time to run and // detect the gap. for (int l = 0; l < 5; ++l) { env.close(); // DO NOT CALL rendezvous()! You'll end up with a deadlock. ++maxSeq; // Nothing has changed BEAST_EXPECTS( store.getLastRotated() == lastRotated, failureMessage("lastRotated", lastRotated, store.getLastRotated())); BEAST_EXPECTS( lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq), failureMessage( "Complete Ledgers", expectedRange(minSeq, deleteSeq, maxSeq), lm.getCompleteLedgers())); std::this_thread::sleep_for(1s); } // Put the missing ledger back in LedgerMaster lm.setLedgerRangePresent(deleteSeq, deleteSeq); // Wait for the rotation to finish store.rendezvous(); minSeq = lastRotated; lastRotated = deleteSeq + 1; } BEAST_EXPECT(maxSeq != lastRotated + kDELETE_INTERVAL); BEAST_EXPECTS( env.closed()->header().seq == maxSeq, failureMessage("maxSeq", maxSeq, env.closed()->header().seq)); BEAST_EXPECTS( store.getLastRotated() == lastRotated, failureMessage("lastRotated", lastRotated, store.getLastRotated())); std::stringstream expectedRange; expectedRange << minSeq << "-" << maxSeq; BEAST_EXPECTS( lm.getCompleteLedgers() == expectedRange.str(), failureMessage("CompleteLedgers", expectedRange.str(), lm.getCompleteLedgers())); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2); BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2); } } void run() override { testClear(); testAutomatic(); testCanDelete(); testRotate(); testLedgerGaps(); } }; // VFALCO This test fails because of thread asynchronous issues BEAST_DEFINE_TESTSUITE(SHAMapStore, app, xrpl); } // namespace xrpl::test