From cc452dfa9b636e0656f327db72db70f1b2c46c23 Mon Sep 17 00:00:00 2001 From: Miguel Portilla Date: Fri, 13 Sep 2019 18:44:24 -0400 Subject: [PATCH 01/14] Improve shard concurrency: * Reduce lock scope on all public functions * Use TaskQueue to process shard finalization in separate thread * Store shard last ledger hash and other info in backend * Use temp SQLite DB versus control file when acquiring * Remove boost serialization from cmake files --- Builds/CMake/RippleConfig.cmake | 1 - Builds/CMake/RippledCore.cmake | 1 + Builds/CMake/RippledInterface.cmake | 4 +- Builds/CMake/RippledSettings.cmake | 6 - Builds/CMake/deps/Boost.cmake | 2 - Builds/containers/shared/install_boost.sh | 1 - src/ripple/app/ledger/impl/InboundLedger.cpp | 2 +- src/ripple/app/ledger/impl/InboundLedgers.cpp | 4 +- src/ripple/app/ledger/impl/LedgerMaster.cpp | 8 +- src/ripple/app/main/Application.cpp | 63 +- src/ripple/app/main/DBInit.h | 41 +- src/ripple/app/main/Main.cpp | 2 +- src/ripple/app/misc/SHAMapStoreImp.cpp | 5 +- src/ripple/basics/RangeSet.h | 143 +- src/ripple/basics/TaggedCache.h | 3 - src/ripple/core/Stoppable.h | 14 +- src/ripple/core/impl/JobQueue.cpp | 2 +- src/ripple/core/impl/Workers.cpp | 5 +- src/ripple/core/impl/Workers.h | 4 +- src/ripple/net/impl/RPCCall.cpp | 1 - src/ripple/nodestore/Database.h | 27 +- src/ripple/nodestore/DatabaseRotating.h | 8 +- src/ripple/nodestore/DatabaseShard.h | 15 +- src/ripple/nodestore/NodeObject.h | 6 +- src/ripple/nodestore/impl/Database.cpp | 77 +- src/ripple/nodestore/impl/DatabaseNodeImp.cpp | 3 - src/ripple/nodestore/impl/DatabaseNodeImp.h | 12 +- .../nodestore/impl/DatabaseRotatingImp.cpp | 17 +- .../nodestore/impl/DatabaseRotatingImp.h | 26 +- .../nodestore/impl/DatabaseShardImp.cpp | 1326 ++++++++++------- src/ripple/nodestore/impl/DatabaseShardImp.h | 96 +- src/ripple/nodestore/impl/NodeObject.cpp | 2 +- src/ripple/nodestore/impl/Shard.cpp | 821 ++++++---- src/ripple/nodestore/impl/Shard.h | 165 +- src/ripple/nodestore/impl/TaskQueue.cpp | 66 + src/ripple/nodestore/impl/TaskQueue.h | 62 + src/ripple/overlay/impl/PeerImp.cpp | 74 +- src/ripple/protocol/jss.h | 1 - src/ripple/rpc/ShardArchiveHandler.h | 4 +- src/ripple/rpc/handlers/DownloadShard.cpp | 14 +- src/ripple/rpc/impl/ShardArchiveHandler.cpp | 7 +- src/ripple/shamap/impl/SHAMapNodeID.cpp | 18 - src/test/basics/RangeSet_test.cpp | 84 +- src/test/core/Workers_test.cpp | 2 +- src/test/nodestore/Database_test.cpp | 4 +- src/test/nodestore/TestBase.h | 2 +- src/test/rpc/RPCCall_test.cpp | 15 +- 47 files changed, 1946 insertions(+), 1320 deletions(-) create mode 100644 src/ripple/nodestore/impl/TaskQueue.cpp create mode 100644 src/ripple/nodestore/impl/TaskQueue.h diff --git a/Builds/CMake/RippleConfig.cmake b/Builds/CMake/RippleConfig.cmake index 1091e741e74..1dc75cb9093 100644 --- a/Builds/CMake/RippleConfig.cmake +++ b/Builds/CMake/RippleConfig.cmake @@ -21,7 +21,6 @@ find_dependency (Boost 1.70 filesystem program_options regex - serialization system thread) #[=========================================================[ diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 35a015d5464..d55ce55801a 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -513,6 +513,7 @@ target_sources (rippled PRIVATE src/ripple/nodestore/impl/ManagerImp.cpp src/ripple/nodestore/impl/NodeObject.cpp src/ripple/nodestore/impl/Shard.cpp + src/ripple/nodestore/impl/TaskQueue.cpp #[===============================[ main sources: subdir: overlay diff --git a/Builds/CMake/RippledInterface.cmake b/Builds/CMake/RippledInterface.cmake index c28896087ee..e0c0c1e5c0f 100644 --- a/Builds/CMake/RippledInterface.cmake +++ b/Builds/CMake/RippledInterface.cmake @@ -21,9 +21,7 @@ target_compile_definitions (opts > $<$:BEAST_NO_UNIT_TEST_INLINE=1> $<$:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1> - $<$:RIPPLE_SINGLE_IO_SERVICE_THREAD=1> - # doesn't currently compile ? : - $<$:RIPPLE_VERIFY_NODEOBJECT_KEYS=1>) + $<$:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>) target_compile_options (opts INTERFACE $<$,$>:-Wsuggest-override> diff --git a/Builds/CMake/RippledSettings.cmake b/Builds/CMake/RippledSettings.cmake index 0fe3354f395..cd17d86552a 100644 --- a/Builds/CMake/RippledSettings.cmake +++ b/Builds/CMake/RippledSettings.cmake @@ -100,12 +100,6 @@ option (have_package_container option (beast_no_unit_test_inline "Prevents unit test definitions from being inserted into global table" OFF) -# NOTE - THIS OPTION CURRENTLY DOES NOT COMPILE : -# TODO: fix or remove -option (verify_nodeobject_keys - "This verifies that the hash of node objects matches the payload. \ - This check is expensive - use with caution." - OFF) option (single_io_service_thread "Restricts the number of threads calling io_service::run to one. \ This can be useful when debugging." diff --git a/Builds/CMake/deps/Boost.cmake b/Builds/CMake/deps/Boost.cmake index e3e8d92d85e..bdff36909cc 100644 --- a/Builds/CMake/deps/Boost.cmake +++ b/Builds/CMake/deps/Boost.cmake @@ -47,7 +47,6 @@ find_package (Boost 1.70 REQUIRED filesystem program_options regex - serialization system thread) @@ -69,7 +68,6 @@ target_link_libraries (ripple_boost Boost::filesystem Boost::program_options Boost::regex - Boost::serialization Boost::system Boost::thread) if (Boost_COMPILER) diff --git a/Builds/containers/shared/install_boost.sh b/Builds/containers/shared/install_boost.sh index 51d6524d785..ea26220e627 100755 --- a/Builds/containers/shared/install_boost.sh +++ b/Builds/containers/shared/install_boost.sh @@ -39,7 +39,6 @@ else BLDARGS+=(--with-filesystem) BLDARGS+=(--with-program_options) BLDARGS+=(--with-regex) - BLDARGS+=(--with-serialization) BLDARGS+=(--with-system) BLDARGS+=(--with-atomic) BLDARGS+=(--with-thread) diff --git a/src/ripple/app/ledger/impl/InboundLedger.cpp b/src/ripple/app/ledger/impl/InboundLedger.cpp index 266695aaf56..db5465593dc 100644 --- a/src/ripple/app/ledger/impl/InboundLedger.cpp +++ b/src/ripple/app/ledger/impl/InboundLedger.cpp @@ -110,7 +110,7 @@ InboundLedger::init(ScopedLockType& collectionLock) if (mFailed) return; } - else if (shardStore && mSeq >= shardStore->earliestSeq()) + else if (shardStore && mSeq >= shardStore->earliestLedgerSeq()) { if (auto l = shardStore->fetchLedger(mHash, mSeq)) { diff --git a/src/ripple/app/ledger/impl/InboundLedgers.cpp b/src/ripple/app/ledger/impl/InboundLedgers.cpp index 589dfc3d79f..c126bc9c325 100644 --- a/src/ripple/app/ledger/impl/InboundLedgers.cpp +++ b/src/ripple/app/ledger/impl/InboundLedgers.cpp @@ -106,7 +106,7 @@ class InboundLedgersImp if (reason == InboundLedger::Reason::HISTORY) { if (inbound->getLedger()->stateMap().family().isShardBacked()) - app_.getNodeStore().copyLedger(inbound->getLedger()); + app_.getNodeStore().storeLedger(inbound->getLedger()); } else if (reason == InboundLedger::Reason::SHARD) { @@ -120,7 +120,7 @@ class InboundLedgersImp if (inbound->getLedger()->stateMap().family().isShardBacked()) shardStore->setStored(inbound->getLedger()); else - shardStore->copyLedger(inbound->getLedger()); + shardStore->storeLedger(inbound->getLedger()); } return inbound->getLedger(); } diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 20f52b4d2cf..f0f63e877fd 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -1742,7 +1742,7 @@ LedgerMaster::fetchForHistory( *hash, missing, reason); if (!ledger && missing != fetch_seq_ && - missing > app_.getNodeStore().earliestSeq()) + missing > app_.getNodeStore().earliestLedgerSeq()) { JLOG(m_journal.trace()) << "fetchForHistory want fetch pack " << missing; @@ -1771,7 +1771,7 @@ LedgerMaster::fetchForHistory( mShardLedger = ledger; } if (!ledger->stateMap().family().isShardBacked()) - app_.getShardStore()->copyLedger(ledger); + app_.getShardStore()->storeLedger(ledger); } else { @@ -1807,7 +1807,7 @@ LedgerMaster::fetchForHistory( else // Do not fetch ledger sequences lower // than the earliest ledger sequence - fetchSz = app_.getNodeStore().earliestSeq(); + fetchSz = app_.getNodeStore().earliestLedgerSeq(); fetchSz = missing >= fetchSz ? std::min(ledger_fetch_size_, (missing - fetchSz) + 1) : 0; try @@ -1867,7 +1867,7 @@ void LedgerMaster::doAdvance (std::unique_lock& sl) std::lock_guard sll(mCompleteLock); missing = prevMissing(mCompleteLedgers, mPubLedger->info().seq, - app_.getNodeStore().earliestSeq()); + app_.getNodeStore().earliestLedgerSeq()); } if (missing) { diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index fcb0961ade0..93042ce5ebc 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -345,9 +345,9 @@ class ApplicationImp // These are Stoppable-related std::unique_ptr m_jobQueue; std::unique_ptr m_nodeStore; - std::unique_ptr shardStore_; detail::AppFamily family_; - std::unique_ptr sFamily_; + std::unique_ptr shardStore_; + std::unique_ptr shardFamily_; // VFALCO TODO Make OrderBookDB abstract OrderBookDB m_orderBookDB; std::unique_ptr m_pathRequests; @@ -463,18 +463,18 @@ class ApplicationImp m_collectorManager->group ("jobq"), m_nodeStoreScheduler, logs_->journal("JobQueue"), *logs_, *perfLog_)) - , m_nodeStore(m_shaMapStore->makeNodeStore("NodeStore.main", 4)) + , m_nodeStore (m_shaMapStore->makeNodeStore ("NodeStore.main", 4)) + + , family_ (*this, *m_nodeStore, *m_collectorManager) // The shard store is optional and make_ShardStore can return null. - , shardStore_(make_ShardStore( + , shardStore_ (make_ShardStore ( *this, *m_jobQueue, m_nodeStoreScheduler, 4, logs_->journal("ShardStore"))) - , family_ (*this, *m_nodeStore, *m_collectorManager) - , m_orderBookDB (*this, *m_jobQueue) , m_pathRequests (std::make_unique ( @@ -558,14 +558,6 @@ class ApplicationImp logs_->journal("Application"), std::chrono::milliseconds (100), get_io_service()) , grpcServer_(std::make_unique(*this)) { - if (shardStore_) - { - sFamily_ = std::make_unique( - *this, - *shardStore_, - *m_collectorManager); - } - add (m_resourceManager.get ()); // @@ -626,7 +618,7 @@ class ApplicationImp Family* shardFamily() override { - return sFamily_.get(); + return shardFamily_.get(); } TimeKeeper& @@ -943,7 +935,7 @@ class ApplicationImp } bool - initNodeStoreDBs() + initNodeStore() { if (config_->doImport) { @@ -961,12 +953,12 @@ class ApplicationImp JLOG(j.warn()) << "Starting node import from '" << source->getName() << - "' to '" << getNodeStore().getName() << "'."; + "' to '" << m_nodeStore->getName() << "'."; using namespace std::chrono; auto const start = steady_clock::now(); - getNodeStore().import(*source); + m_nodeStore->import(*source); auto const elapsed = duration_cast (steady_clock::now() - start); @@ -990,14 +982,6 @@ class ApplicationImp family().treecache().setTargetAge( seconds{config_->getValueFor(SizedItem::treeCacheAge)}); - if (sFamily_) - { - sFamily_->treecache().setTargetSize( - config_->getValueFor(SizedItem::treeCacheSize)); - sFamily_->treecache().setTargetAge( - seconds{config_->getValueFor(SizedItem::treeCacheAge)}); - } - return true; } @@ -1252,8 +1236,8 @@ class ApplicationImp // have listeners register for "onSweep ()" notification. family().fullbelow().sweep(); - if (sFamily_) - sFamily_->fullbelow().sweep(); + if (shardFamily_) + shardFamily_->fullbelow().sweep(); getMasterTransaction().sweep(); getNodeStore().sweep(); if (shardStore_) @@ -1264,8 +1248,8 @@ class ApplicationImp getInboundLedgers().sweep(); m_acceptedLedgerCache.sweep(); family().treecache().sweep(); - if (sFamily_) - sFamily_->treecache().sweep(); + if (shardFamily_) + shardFamily_->treecache().sweep(); cachedSLEs_.expire(); // Set timer to do another sweep later. @@ -1350,9 +1334,26 @@ bool ApplicationImp::setup() if (!config_->standalone()) timeKeeper_->run(config_->SNTP_SERVERS); - if (!initSQLiteDBs() || !initNodeStoreDBs()) + if (!initSQLiteDBs() || !initNodeStore()) return false; + if (shardStore_) + { + shardFamily_ = std::make_unique( + *this, + *shardStore_, + *m_collectorManager); + + using namespace std::chrono; + shardFamily_->treecache().setTargetSize( + config_->getValueFor(SizedItem::treeCacheSize)); + shardFamily_->treecache().setTargetAge( + seconds{config_->getValueFor(SizedItem::treeCacheAge)}); + + if (!shardStore_->init()) + return false; + } + if (!peerReservations_->load(getWalletDB())) { JLOG(m_journal.fatal()) << "Cannot find peer reservations!"; diff --git a/src/ripple/app/main/DBInit.h b/src/ripple/app/main/DBInit.h index b632d168bf3..af693f708b3 100644 --- a/src/ripple/app/main/DBInit.h +++ b/src/ripple/app/main/DBInit.h @@ -27,16 +27,16 @@ namespace ripple { //////////////////////////////////////////////////////////////////////////////// // Ledger database holds ledgers and ledger confirmations -static constexpr auto LgrDBName {"ledger.db"}; +inline constexpr auto LgrDBName {"ledger.db"}; -static constexpr +inline constexpr std::array LgrDBPragma {{ "PRAGMA synchronous=NORMAL;", "PRAGMA journal_mode=WAL;", "PRAGMA journal_size_limit=1582080;" }}; -static constexpr +inline constexpr std::array LgrDBInit {{ "BEGIN TRANSACTION;", @@ -63,9 +63,9 @@ std::array LgrDBInit {{ //////////////////////////////////////////////////////////////////////////////// // Transaction database holds transactions and public keys -static constexpr auto TxDBName {"transaction.db"}; +inline constexpr auto TxDBName {"transaction.db"}; -static constexpr +inline constexpr #if (ULONG_MAX > UINT_MAX) && !defined (NO_SQLITE_MMAP) std::array TxDBPragma {{ #else @@ -81,7 +81,7 @@ static constexpr #endif }}; -static constexpr +inline constexpr std::array TxDBInit {{ "BEGIN TRANSACTION;", @@ -116,18 +116,39 @@ std::array TxDBInit {{ //////////////////////////////////////////////////////////////////////////////// +// Temporary database used with an incomplete shard that is being acquired +inline constexpr auto AcquireShardDBName {"acquire.db"}; + +inline constexpr +std::array AcquireShardDBPragma {{ + "PRAGMA synchronous=NORMAL;", + "PRAGMA journal_mode=WAL;", + "PRAGMA journal_size_limit=1582080;" +}}; + +inline constexpr +std::array AcquireShardDBInit {{ + "CREATE TABLE IF NOT EXISTS Shard ( \ + ShardIndex INTEGER PRIMARY KEY, \ + LastLedgerHash CHARACTER(64), \ + StoredLedgerSeqs BLOB \ + );" +}}; + +//////////////////////////////////////////////////////////////////////////////// + // Pragma for Ledger and Transaction databases with complete shards -static constexpr -std::array CompleteShardDBPragma {{ +inline constexpr +std::array CompleteShardDBPragma{{ "PRAGMA synchronous=OFF;", "PRAGMA journal_mode=OFF;" }}; //////////////////////////////////////////////////////////////////////////////// -static constexpr auto WalletDBName {"wallet.db"}; +inline constexpr auto WalletDBName {"wallet.db"}; -static constexpr +inline constexpr std::array WalletDBInit {{ "BEGIN TRANSACTION;", diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 21267db99d5..cfc824915d0 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -142,7 +142,7 @@ void printHelp (const po::options_description& desc) " connect []\n" " consensus_info\n" " deposit_authorized []\n" - " download_shard [[ ]] \n" + " download_shard [[ ]]\n" " feature [ [accept|reject]]\n" " fetch_info [clear]\n" " gateway_balances [] [ [ ]]\n" diff --git a/src/ripple/app/misc/SHAMapStoreImp.cpp b/src/ripple/app/misc/SHAMapStoreImp.cpp index 7e32ff0f890..0027bd021c7 100644 --- a/src/ripple/app/misc/SHAMapStoreImp.cpp +++ b/src/ripple/app/misc/SHAMapStoreImp.cpp @@ -449,7 +449,7 @@ SHAMapStoreImp::run() std::string nextArchiveDir = dbRotating_->getWritableBackend()->getName(); lastRotated = validatedSeq; - std::unique_ptr oldBackend; + std::shared_ptr oldBackend; { std::lock_guard lock (dbRotating_->peekMutex()); @@ -457,7 +457,8 @@ SHAMapStoreImp::run() nextArchiveDir, lastRotated}); clearCaches (validatedSeq); oldBackend = dbRotating_->rotateBackends( - std::move(newBackend)); + std::move(newBackend), + lock); } JLOG(journal_.warn()) << "finished rotation " << validatedSeq; diff --git a/src/ripple/basics/RangeSet.h b/src/ripple/basics/RangeSet.h index 13f58c94ad9..4e00a4627ed 100644 --- a/src/ripple/basics/RangeSet.h +++ b/src/ripple/basics/RangeSet.h @@ -20,11 +20,14 @@ #ifndef RIPPLE_BASICS_RANGESET_H_INCLUDED #define RIPPLE_BASICS_RANGESET_H_INCLUDED -#include -#include +#include + +#include #include #include -#include +#include + +#include namespace ripple { @@ -86,8 +89,8 @@ std::string to_string(ClosedInterval const & ci) /** Convert the given RangeSet to a styled string. - The styled string represention is the set of disjoint intervals joined by - commas. The string "empty" is returned if the set is empty. + The styled string representation is the set of disjoint intervals joined + by commas. The string "empty" is returned if the set is empty. @param rs The rangeset to convert @return The styled string @@ -109,6 +112,67 @@ std::string to_string(RangeSet const & rs) return res; } +/** Convert the given styled string to a RangeSet. + + The styled string representation is the set + of disjoint intervals joined by commas. + + @param rs The set to be populated + @param s The styled string to convert + @return True on successfully converting styled string +*/ +template +bool +from_string(RangeSet& rs, std::string const& s) +{ + std::vector intervals; + std::vector tokens; + bool result {true}; + + boost::split(tokens, s, boost::algorithm::is_any_of(",")); + for (auto const& t : tokens) + { + boost::split(intervals, t, boost::algorithm::is_any_of("-")); + switch (intervals.size()) + { + case 1: + { + T front; + if (!beast::lexicalCastChecked(front, intervals.front())) + result = false; + else + rs.insert(front); + break; + } + case 2: + { + T front; + if (!beast::lexicalCastChecked(front, intervals.front())) + result = false; + else + { + T back; + if (!beast::lexicalCastChecked(back, intervals.back())) + result = false; + else + rs.insert(range(front, back)); + } + break; + } + default: + result = false; + } + + if (!result) + break; + intervals.clear(); + } + + if (!result) + rs.clear(); + return result; +} + /** Find the largest value not in the set that is less than a given value. @param rs The set of interest @@ -129,75 +193,8 @@ prevMissing(RangeSet const & rs, T t, T minVal = 0) return boost::none; return boost::icl::last(tgt); } -} // namespace ripple - - -// The boost serialization documents recommended putting free-function helpers -// in the boost serialization namespace -namespace boost { -namespace serialization { -template -void -save(Archive& ar, - ripple::ClosedInterval const& ci, - const unsigned int version) -{ - auto l = ci.lower(); - auto u = ci.upper(); - ar << l << u; -} - -template -void -load(Archive& ar, ripple::ClosedInterval& ci, const unsigned int version) -{ - T low, up; - ar >> low >> up; - ci = ripple::ClosedInterval{low, up}; -} - -template -void -serialize(Archive& ar, - ripple::ClosedInterval& ci, - const unsigned int version) -{ - split_free(ar, ci, version); -} - -template -void -save(Archive& ar, ripple::RangeSet const& rs, const unsigned int version) -{ - auto s = rs.iterative_size(); - ar << s; - for (auto const& r : rs) - ar << r; -} - -template -void -load(Archive& ar, ripple::RangeSet& rs, const unsigned int version) -{ - rs.clear(); - std::size_t intervals; - ar >> intervals; - for (std::size_t i = 0; i < intervals; ++i) - { - ripple::ClosedInterval ci; - ar >> ci; - rs.insert(ci); - } -} +} // namespace ripple -template -void -serialize(Archive& ar, ripple::RangeSet& rs, const unsigned int version) -{ - split_free(ar, rs, version); -} -} // serialization -} // boost #endif diff --git a/src/ripple/basics/TaggedCache.h b/src/ripple/basics/TaggedCache.h index 3ed6bd1c3bb..2d1723c839e 100644 --- a/src/ripple/basics/TaggedCache.h +++ b/src/ripple/basics/TaggedCache.h @@ -31,9 +31,6 @@ namespace ripple { -// VFALCO NOTE Deprecated -struct TaggedCacheLog; - /** Map/cache combination. This class implements a cache and a map. The cache keeps objects alive in the map. The map allows multiple code paths that reference objects diff --git a/src/ripple/core/Stoppable.h b/src/ripple/core/Stoppable.h index cca36e013a4..4d795147f81 100644 --- a/src/ripple/core/Stoppable.h +++ b/src/ripple/core/Stoppable.h @@ -186,13 +186,13 @@ class RootStoppable; | JobQueue | - +-----------+-----------+-----------+-----------+----+--------+ - | | | | | | - | NetworkOPs | InboundLedgers | OrderbookDB - | | | - Overlay InboundTransactions LedgerMaster - | | - PeerFinder LedgerCleaner + +--------+-----------+-----------+-----------+-------+---+----------+ + | | | | | | | + | NetworkOPs | InboundLedgers | OrderbookDB | + | | | | + Overlay InboundTransactions LedgerMaster Database + | | | + PeerFinder LedgerCleaner TaskQueue @endcode */ diff --git a/src/ripple/core/impl/JobQueue.cpp b/src/ripple/core/impl/JobQueue.cpp index c418bc67a03..3cf796f06e1 100644 --- a/src/ripple/core/impl/JobQueue.cpp +++ b/src/ripple/core/impl/JobQueue.cpp @@ -31,7 +31,7 @@ JobQueue::JobQueue (beast::insight::Collector::ptr const& collector, , m_lastJob (0) , m_invalidJobData (JobTypes::instance().getInvalid (), collector, logs) , m_processCount (0) - , m_workers (*this, perfLog, "JobQueue", 0) + , m_workers (*this, &perfLog, "JobQueue", 0) , m_cancelCallback (std::bind (&Stoppable::isStopping, this)) , perfLog_ (perfLog) , m_collector (collector) diff --git a/src/ripple/core/impl/Workers.cpp b/src/ripple/core/impl/Workers.cpp index ca456de5728..f04f94e4b84 100644 --- a/src/ripple/core/impl/Workers.cpp +++ b/src/ripple/core/impl/Workers.cpp @@ -26,7 +26,7 @@ namespace ripple { Workers::Workers ( Callback& callback, - perf::PerfLog& perfLog, + perf::PerfLog* perfLog, std::string const& threadNames, int numberOfThreads) : m_callback (callback) @@ -63,7 +63,8 @@ void Workers::setNumberOfThreads (int numberOfThreads) static int instance {0}; if (m_numberOfThreads != numberOfThreads) { - perfLog_.resizeJobs(numberOfThreads); + if (perfLog_) + perfLog_->resizeJobs(numberOfThreads); if (numberOfThreads > m_numberOfThreads) { diff --git a/src/ripple/core/impl/Workers.h b/src/ripple/core/impl/Workers.h index 9721ae9e6e2..3a811ce899c 100644 --- a/src/ripple/core/impl/Workers.h +++ b/src/ripple/core/impl/Workers.h @@ -69,7 +69,7 @@ class Workers @param threadNames The name given to each created worker thread. */ explicit Workers (Callback& callback, - perf::PerfLog& perfLog, + perf::PerfLog* perfLog, std::string const& threadNames = "Worker", int numberOfThreads = static_cast(std::thread::hardware_concurrency())); @@ -166,7 +166,7 @@ class Workers private: Callback& m_callback; - perf::PerfLog& perfLog_; + perf::PerfLog* perfLog_; std::string m_threadNames; // The name to give each thread std::condition_variable m_cv; // signaled when all threads paused std::mutex m_mut; diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 64f2941653f..bca8fefb5b7 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -186,7 +186,6 @@ class RPCParser ++i; else if (!boost::iequals(jvParams[--sz].asString(), "novalidate")) return rpcError(rpcINVALID_PARAMS); - jvResult[jss::validate] = false; } // Create the 'shards' array diff --git a/src/ripple/nodestore/Database.h b/src/ripple/nodestore/Database.h index 1dde5f4df47..3d91d45a020 100644 --- a/src/ripple/nodestore/Database.h +++ b/src/ripple/nodestore/Database.h @@ -149,7 +149,7 @@ class Database : public Stoppable */ virtual bool - copyLedger(std::shared_ptr const& ledger) = 0; + storeLedger(std::shared_ptr const& srcLedger) = 0; /** Wait for all currently pending async reads to complete. */ @@ -211,12 +211,15 @@ class Database : public Stoppable void onStop() override; + void + onChildrenStopped() override; + /** @return The earliest ledger sequence allowed */ std::uint32_t - earliestSeq() const + earliestLedgerSeq() const { - return earliestSeq_; + return earliestLedgerSeq_; } protected: @@ -234,14 +237,17 @@ class Database : public Stoppable storeSz_ += sz; } + // Called by the public asyncFetch function void asyncFetch(uint256 const& hash, std::uint32_t seq, std::shared_ptr> const& pCache, std::shared_ptr> const& nCache); + // Called by the public fetch function std::shared_ptr - fetchInternal(uint256 const& hash, Backend& srcBackend); + fetchInternal(uint256 const& hash, std::shared_ptr backend); + // Called by the public import function void importInternal(Backend& dstBackend, Database& srcDB); @@ -250,11 +256,14 @@ class Database : public Stoppable TaggedCache& pCache, KeyCache& nCache, bool isAsync); + // Called by the public storeLedger function bool - copyLedger(Backend& dstBackend, Ledger const& srcLedger, - std::shared_ptr> const& pCache, - std::shared_ptr> const& nCache, - std::shared_ptr const& srcNext); + storeLedger( + Ledger const& srcLedger, + std::shared_ptr dstBackend, + std::shared_ptr> dstPCache, + std::shared_ptr> dstNCache, + std::shared_ptr next); private: std::atomic storeCount_ {0}; @@ -283,7 +292,7 @@ class Database : public Stoppable // The default is 32570 to match the XRP ledger network's earliest // allowed sequence. Alternate networks may set this value. - std::uint32_t const earliestSeq_; + std::uint32_t const earliestLedgerSeq_; virtual std::shared_ptr diff --git a/src/ripple/nodestore/DatabaseRotating.h b/src/ripple/nodestore/DatabaseRotating.h index 75606be187e..b44c6849c23 100644 --- a/src/ripple/nodestore/DatabaseRotating.h +++ b/src/ripple/nodestore/DatabaseRotating.h @@ -50,12 +50,14 @@ class DatabaseRotating : public Database virtual std::mutex& peekMutex() const = 0; virtual - std::unique_ptr const& + std::shared_ptr const& getWritableBackend() const = 0; virtual - std::unique_ptr - rotateBackends(std::unique_ptr newBackend) = 0; + std::shared_ptr + rotateBackends( + std::shared_ptr newBackend, + std::lock_guard const&) = 0; }; } diff --git a/src/ripple/nodestore/DatabaseShard.h b/src/ripple/nodestore/DatabaseShard.h index a6bd7dd283f..0e86664ca8c 100644 --- a/src/ripple/nodestore/DatabaseShard.h +++ b/src/ripple/nodestore/DatabaseShard.h @@ -109,14 +109,14 @@ class DatabaseShard : public Database @param shardIndex Shard index to import @param srcDir The directory to import from - @param validate If true validate shard ledger data @return true If the shard was successfully imported @implNote if successful, srcDir is moved to the database directory */ virtual bool - importShard(std::uint32_t shardIndex, - boost::filesystem::path const& srcDir, bool validate) = 0; + importShard( + std::uint32_t shardIndex, + boost::filesystem::path const& srcDir) = 0; /** Fetch a ledger from the shard store @@ -137,15 +137,6 @@ class DatabaseShard : public Database void setStored(std::shared_ptr const& ledger) = 0; - /** Query if a ledger with the given sequence is stored - - @param seq The ledger sequence to check if stored - @return `true` if the ledger is stored - */ - virtual - bool - contains(std::uint32_t seq) = 0; - /** Query which complete shards are stored @return the indexes of complete shards diff --git a/src/ripple/nodestore/NodeObject.h b/src/ripple/nodestore/NodeObject.h index caacaae3f96..90438deb3e4 100644 --- a/src/ripple/nodestore/NodeObject.h +++ b/src/ripple/nodestore/NodeObject.h @@ -95,9 +95,9 @@ class NodeObject : public CountedObject Blob const& getData () const; private: - NodeObjectType mType; - uint256 mHash; - Blob mData; + NodeObjectType const mType; + uint256 const mHash; + Blob const mData; }; } diff --git a/src/ripple/nodestore/impl/Database.cpp b/src/ripple/nodestore/impl/Database.cpp index f6a3c3785b0..1e8e01195c3 100644 --- a/src/ripple/nodestore/impl/Database.cpp +++ b/src/ripple/nodestore/impl/Database.cpp @@ -36,12 +36,12 @@ Database::Database( : Stoppable(name, parent.getRoot()) , j_(journal) , scheduler_(scheduler) - , earliestSeq_(get( + , earliestLedgerSeq_(get( config, "earliest_seq", XRP_LEDGER_EARLIEST_SEQ)) { - if (earliestSeq_ < 1) + if (earliestLedgerSeq_ < 1) Throw("Invalid earliest_seq"); while (readThreads-- > 0) @@ -83,6 +83,11 @@ Database::onStop() // After stop time we can no longer use the JobQueue for background // reads. Join the background read threads. stopThreads(); +} + +void +Database::onChildrenStopped() +{ stopped(); } @@ -115,13 +120,13 @@ Database::asyncFetch(uint256 const& hash, std::uint32_t seq, } std::shared_ptr -Database::fetchInternal(uint256 const& hash, Backend& srcBackend) +Database::fetchInternal(uint256 const& hash, std::shared_ptr backend) { std::shared_ptr nObj; Status status; try { - status = srcBackend.fetch(hash.begin(), &nObj); + status = backend->fetch(hash.begin(), &nObj); } catch (std::exception const& e) { @@ -226,12 +231,14 @@ Database::doFetch(uint256 const& hash, std::uint32_t seq, } bool -Database::copyLedger(Backend& dstBackend, Ledger const& srcLedger, - std::shared_ptr> const& pCache, - std::shared_ptr> const& nCache, - std::shared_ptr const& srcNext) +Database::storeLedger( + Ledger const& srcLedger, + std::shared_ptr dstBackend, + std::shared_ptr> dstPCache, + std::shared_ptr> dstNCache, + std::shared_ptr next) { - assert(static_cast(pCache) == static_cast(nCache)); + assert(static_cast(dstPCache) == static_cast(dstNCache)); if (srcLedger.info().hash.isZero() || srcLedger.info().accountHash.isZero()) { @@ -254,48 +261,42 @@ Database::copyLedger(Backend& dstBackend, Ledger const& srcLedger, Batch batch; batch.reserve(batchWritePreallocationSize); auto storeBatch = [&]() { -#if RIPPLE_VERIFY_NODEOBJECT_KEYS - for (auto& nObj : batch) + if (dstPCache && dstNCache) { - assert(nObj->getHash() == - sha512Hash(makeSlice(nObj->getData()))); - if (pCache && nCache) - { - pCache->canonicalize(nObj->getHash(), nObj, true); - nCache->erase(nObj->getHash()); - storeStats(nObj->getData().size()); - } - } -#else - if (pCache && nCache) for (auto& nObj : batch) { - pCache->canonicalize(nObj->getHash(), nObj, true); - nCache->erase(nObj->getHash()); + dstPCache->canonicalize(nObj->getHash(), nObj, true); + dstNCache->erase(nObj->getHash()); storeStats(nObj->getData().size()); } -#endif - dstBackend.storeBatch(batch); + } + dstBackend->storeBatch(batch); batch.clear(); batch.reserve(batchWritePreallocationSize); }; bool error = false; - auto f = [&](SHAMapAbstractNode& node) { + auto visit = [&](SHAMapAbstractNode& node) + { if (auto nObj = srcDB.fetch( node.getNodeHash().as_uint256(), srcLedger.info().seq)) { batch.emplace_back(std::move(nObj)); - if (batch.size() >= batchWritePreallocationSize) - storeBatch(); + if (batch.size() < batchWritePreallocationSize) + return true; + + storeBatch(); + + if (!isStopping()) + return true; } - else - error = true; - return !error; + + error = true; + return false; }; // Store ledger header { - Serializer s(1024); + Serializer s(sizeof(std::uint32_t) + sizeof(LedgerInfo)); s.add32(HashPrefix::ledgerMaster); addRaw(srcLedger.info(), s); auto nObj = NodeObject::createObject(hotLEDGER, @@ -313,14 +314,14 @@ Database::copyLedger(Backend& dstBackend, Ledger const& srcLedger, " state map invalid"; return false; } - if (srcNext && srcNext->info().parentHash == srcLedger.info().hash) + if (next && next->info().parentHash == srcLedger.info().hash) { - auto have = srcNext->stateMap().snapShot(false); + auto have = next->stateMap().snapShot(false); srcLedger.stateMap().snapShot( - false)->visitDifferences(&(*have), f); + false)->visitDifferences(&(*have), visit); } else - srcLedger.stateMap().snapShot(false)->visitNodes(f); + srcLedger.stateMap().snapShot(false)->visitNodes(visit); if (error) return false; } @@ -335,7 +336,7 @@ Database::copyLedger(Backend& dstBackend, Ledger const& srcLedger, " transaction map invalid"; return false; } - srcLedger.txMap().snapShot(false)->visitNodes(f); + srcLedger.txMap().snapShot(false)->visitNodes(visit); if (error) return false; } diff --git a/src/ripple/nodestore/impl/DatabaseNodeImp.cpp b/src/ripple/nodestore/impl/DatabaseNodeImp.cpp index 6c3c3e3a545..165b826d585 100644 --- a/src/ripple/nodestore/impl/DatabaseNodeImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseNodeImp.cpp @@ -28,9 +28,6 @@ void DatabaseNodeImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t seq) { -#if RIPPLE_VERIFY_NODEOBJECT_KEYS - assert(hash == sha512Hash(makeSlice(data))); -#endif auto nObj = NodeObject::createObject(type, std::move(data), hash); pCache_->canonicalize(hash, nObj, true); backend_->store(nObj); diff --git a/src/ripple/nodestore/impl/DatabaseNodeImp.h b/src/ripple/nodestore/impl/DatabaseNodeImp.h index 4e62f9ace43..7543434e227 100644 --- a/src/ripple/nodestore/impl/DatabaseNodeImp.h +++ b/src/ripple/nodestore/impl/DatabaseNodeImp.h @@ -38,7 +38,7 @@ class DatabaseNodeImp : public Database Scheduler& scheduler, int readThreads, Stoppable& parent, - std::unique_ptr backend, + std::shared_ptr backend, Section const& config, beast::Journal j) : Database(name, parent, scheduler, readThreads, config, j) @@ -91,10 +91,10 @@ class DatabaseNodeImp : public Database std::shared_ptr& object) override; bool - copyLedger(std::shared_ptr const& ledger) override + storeLedger(std::shared_ptr const& srcLedger) override { - return Database::copyLedger( - *backend_, *ledger, pCache_, nCache_, nullptr); + return Database::storeLedger( + *srcLedger, backend_, pCache_, nCache_, nullptr); } int @@ -123,12 +123,12 @@ class DatabaseNodeImp : public Database std::shared_ptr> nCache_; // Persistent key/value storage - std::unique_ptr backend_; + std::shared_ptr backend_; std::shared_ptr fetchFrom(uint256 const& hash, std::uint32_t seq) override { - return fetchInternal(hash, *backend_); + return fetchInternal(hash, backend_); } void diff --git a/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp b/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp index 76b2b4ec59d..edd23e62010 100644 --- a/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp @@ -29,8 +29,8 @@ DatabaseRotatingImp::DatabaseRotatingImp( Scheduler& scheduler, int readThreads, Stoppable& parent, - std::unique_ptr writableBackend, - std::unique_ptr archiveBackend, + std::shared_ptr writableBackend, + std::shared_ptr archiveBackend, Section const& config, beast::Journal j) : DatabaseRotating(name, parent, scheduler, readThreads, config, j) @@ -48,10 +48,10 @@ DatabaseRotatingImp::DatabaseRotatingImp( setParent(parent); } -// Make sure to call it already locked! -std::unique_ptr +std::shared_ptr DatabaseRotatingImp::rotateBackends( - std::unique_ptr newBackend) + std::shared_ptr newBackend, + std::lock_guard const&) { auto oldBackend {std::move(archiveBackend_)}; archiveBackend_ = std::move(writableBackend_); @@ -63,9 +63,6 @@ void DatabaseRotatingImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t seq) { -#if RIPPLE_VERIFY_NODEOBJECT_KEYS - assert(hash == sha512Hash(makeSlice(data))); -#endif auto nObj = NodeObject::createObject(type, std::move(data), hash); pCache_->canonicalize(hash, nObj, true); getWritableBackend()->store(nObj); @@ -106,10 +103,10 @@ std::shared_ptr DatabaseRotatingImp::fetchFrom(uint256 const& hash, std::uint32_t seq) { Backends b = getBackends(); - auto nObj = fetchInternal(hash, *b.writableBackend); + auto nObj = fetchInternal(hash, b.writableBackend); if (! nObj) { - nObj = fetchInternal(hash, *b.archiveBackend); + nObj = fetchInternal(hash, b.archiveBackend); if (nObj) { getWritableBackend()->store(nObj); diff --git a/src/ripple/nodestore/impl/DatabaseRotatingImp.h b/src/ripple/nodestore/impl/DatabaseRotatingImp.h index e925de5d89d..4cdf6396f87 100644 --- a/src/ripple/nodestore/impl/DatabaseRotatingImp.h +++ b/src/ripple/nodestore/impl/DatabaseRotatingImp.h @@ -37,8 +37,8 @@ class DatabaseRotatingImp : public DatabaseRotating Scheduler& scheduler, int readThreads, Stoppable& parent, - std::unique_ptr writableBackend, - std::unique_ptr archiveBackend, + std::shared_ptr writableBackend, + std::shared_ptr archiveBackend, Section const& config, beast::Journal j); @@ -48,15 +48,17 @@ class DatabaseRotatingImp : public DatabaseRotating stopThreads(); } - std::unique_ptr const& + std::shared_ptr const& getWritableBackend() const override { std::lock_guard lock (rotateMutex_); return writableBackend_; } - std::unique_ptr - rotateBackends(std::unique_ptr newBackend) override; + std::shared_ptr + rotateBackends( + std::shared_ptr newBackend, + std::lock_guard const&) override; std::mutex& peekMutex() const override { @@ -92,10 +94,10 @@ class DatabaseRotatingImp : public DatabaseRotating std::shared_ptr& object) override; bool - copyLedger(std::shared_ptr const& ledger) override + storeLedger(std::shared_ptr const& srcLedger) override { - return Database::copyLedger( - *getWritableBackend(), *ledger, pCache_, nCache_, nullptr); + return Database::storeLedger( + *srcLedger, getWritableBackend(), pCache_, nCache_, nullptr); } int @@ -126,13 +128,13 @@ class DatabaseRotatingImp : public DatabaseRotating // Negative cache std::shared_ptr> nCache_; - std::unique_ptr writableBackend_; - std::unique_ptr archiveBackend_; + std::shared_ptr writableBackend_; + std::shared_ptr archiveBackend_; mutable std::mutex rotateMutex_; struct Backends { - std::unique_ptr const& writableBackend; - std::unique_ptr const& archiveBackend; + std::shared_ptr const& writableBackend; + std::shared_ptr const& archiveBackend; }; Backends getBackends() const diff --git a/src/ripple/nodestore/impl/DatabaseShardImp.cpp b/src/ripple/nodestore/impl/DatabaseShardImp.cpp index 0d28c9cb40e..ae1072b36ae 100644 --- a/src/ripple/nodestore/impl/DatabaseShardImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseShardImp.cpp @@ -20,12 +20,12 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -50,209 +50,174 @@ DatabaseShardImp::DatabaseShardImp( app.config().section(ConfigSection::shardDatabase()), j) , app_(app) - , earliestShardIndex_(seqToShardIndex(earliestSeq())) + , parent_(parent) + , taskQueue_(std::make_unique(*this)) + , earliestShardIndex_(seqToShardIndex(earliestLedgerSeq())) , avgShardFileSz_(ledgersPerShard_ * kilobytes(192)) { } - DatabaseShardImp::~DatabaseShardImp() { - // Stop threads before data members are destroyed - stopThreads(); - - // Close backend databases before destroying the context - std::lock_guard lock(m_); - complete_.clear(); - if (incomplete_) - incomplete_.reset(); - preShards_.clear(); - ctx_.reset(); + onStop(); } bool DatabaseShardImp::init() { - using namespace boost::filesystem; - - std::lock_guard lock(m_); - auto fail = [j = j_](std::string const& msg) { - JLOG(j.error()) << - "[" << ConfigSection::shardDatabase() << "] " << msg; - return false; - }; - - if (init_) - return fail("already initialized"); + std::lock_guard lock(mutex_); + if (init_) + { + JLOG(j_.error()) << "already initialized"; + return false; + } - Config const& config {app_.config()}; - Section const& section {config.section(ConfigSection::shardDatabase())}; - if (section.empty()) - return fail("missing configuration"); + if (!initConfig(lock)) + { + JLOG(j_.error()) << "invalid configuration file settings"; + return false; + } - { - // Node and shard stores must use same earliest ledger sequence - std::uint32_t seq; - if (get_if_exists( - config.section(ConfigSection::nodeDatabase()), - "earliest_seq", - seq)) + try { - std::uint32_t seq2; - if (get_if_exists(section, "earliest_seq", seq2) && - seq != seq2) + using namespace boost::filesystem; + if (exists(dir_)) { - return fail("and [" + ConfigSection::shardDatabase() + - "] both define 'earliest_seq'"); + if (!is_directory(dir_)) + { + JLOG(j_.error()) << "'path' must be a directory"; + return false; + } } - } - } - - if (!get_if_exists(section, "path", dir_)) - return fail("'path' missing"); + else + create_directories(dir_); - if (boost::filesystem::exists(dir_)) - { - if (!boost::filesystem::is_directory(dir_)) - return fail("'path' must be a directory"); - } - else - boost::filesystem::create_directories(dir_); + ctx_ = std::make_unique(); + ctx_->start(); - { - std::uint64_t sz; - if (!get_if_exists(section, "max_size_gb", sz)) - return fail("'max_size_gb' missing"); + // Find shards + for (auto const& d : directory_iterator(dir_)) + { + if (!is_directory(d)) + continue; - if ((sz << 30) < sz) - return fail("'max_size_gb' overflow"); + // Check shard directory name is numeric + auto dirName = d.path().stem().string(); + if (!std::all_of( + dirName.begin(), + dirName.end(), + [](auto c) { + return ::isdigit(static_cast(c)); + })) + { + continue; + } - // Minimum storage space required (in gigabytes) - if (sz < 10) - return fail("'max_size_gb' must be at least 10"); + auto const shardIndex {std::stoul(dirName)}; + if (shardIndex < earliestShardIndex()) + { + JLOG(j_.error()) << + "shard " << shardIndex << + " comes before earliest shard index " << + earliestShardIndex(); + return false; + } - // Convert to bytes - maxFileSz_ = sz << 30; - } + auto const shardDir {dir_ / std::to_string(shardIndex)}; - if (section.exists("ledgers_per_shard")) - { - // To be set only in standalone for testing - if (!config.standalone()) - return fail("'ledgers_per_shard' only honored in stand alone"); + // Check if a previous import failed + if (is_regular_file(shardDir / importMarker_)) + { + JLOG(j_.warn()) << + "shard " << shardIndex << + " previously failed import, removing"; + remove_all(shardDir); + continue; + } - ledgersPerShard_ = get(section, "ledgers_per_shard"); - if (ledgersPerShard_ == 0 || ledgersPerShard_ % 256 != 0) - return fail("'ledgers_per_shard' must be a multiple of 256"); - } + auto shard {std::make_unique( + app_, + *this, + shardIndex, + j_)}; + if (!shard->open(scheduler_, *ctx_)) + { + if (!shard->isLegacy()) + return false; + + // Remove legacy shard + JLOG(j_.warn()) << + "shard " << shardIndex << + " incompatible legacy shard, removing"; + remove_all(shardDir); + continue; + } - // NuDB is the default and only supported permanent storage backend - // "Memory" and "none" types are supported for tests - backendName_ = get(section, "type", "nudb"); - if (!boost::iequals(backendName_, "NuDB") && - !boost::iequals(backendName_, "Memory") && - !boost::iequals(backendName_, "none")) - { - return fail("'type' value unsupported"); - } + if (shard->isFinal()) + { + shards_.emplace( + shardIndex, + ShardInfo(std::move(shard), ShardInfo::State::final)); + } + else if (shard->isBackendComplete()) + { + auto const result {shards_.emplace( + shardIndex, + ShardInfo(std::move(shard), ShardInfo::State::none))}; + finalizeShard(result.first->second, true, lock); + } + else + { + if (acquireIndex_ != 0) + { + JLOG(j_.error()) << + "more than one shard being acquired"; + return false; + } - // Check if backend uses permanent storage - if (auto factory = Manager::instance().find(backendName_)) - { - auto backend {factory->createInstance( - NodeObject::keyBytes, section, scheduler_, j_)}; - backed_ = backend->backed(); - if (!backed_) - { - setFileStats(lock); - init_ = true; - return true; + shards_.emplace( + shardIndex, + ShardInfo(std::move(shard), ShardInfo::State::acquire)); + acquireIndex_ = shardIndex; + } + } } - } - else - return fail(backendName_ + " backend unsupported"); - - try - { - ctx_ = std::make_unique(); - ctx_->start(); - - // Find shards - for (auto const& d : directory_iterator(dir_)) + catch (std::exception const& e) { - if (!is_directory(d)) - continue; - - // Validate shard directory name is numeric - auto dirName = d.path().stem().string(); - if (!std::all_of( - dirName.begin(), - dirName.end(), - [](auto c) { - return ::isdigit(static_cast(c)); - })) - { - continue; - } - - auto const shardIndex {std::stoul(dirName)}; - if (shardIndex < earliestShardIndex()) - { - return fail("shard " + std::to_string(shardIndex) + - " comes before earliest shard index " + - std::to_string(earliestShardIndex())); - } - - // Check if a previous import failed - if (is_regular_file( - dir_ / std::to_string(shardIndex) / importMarker_)) - { - JLOG(j_.warn()) << - "shard " << shardIndex << - " previously failed import, removing"; - remove_all(dir_ / std::to_string(shardIndex)); - continue; - } - - auto shard {std::make_unique(app_, *this, shardIndex, j_)}; - if (!shard->open(scheduler_, *ctx_)) - return false; - - if (shard->complete()) - complete_.emplace(shard->index(), std::move(shard)); - else - { - if (incomplete_) - return fail("more than one control file found"); - incomplete_ = std::move(shard); - } + JLOG(j_.error()) << + "exception " << e.what() << " in function " << __func__; } - } - catch (std::exception const& e) - { - return fail(std::string("exception ") + - e.what() + " in function " + __func__); + + updateStatus(lock); + setParent(parent_); + init_ = true; } - setFileStats(lock); - updateStatus(lock); - init_ = true; + setFileStats(); return true; } boost::optional DatabaseShardImp::prepareLedger(std::uint32_t validLedgerSeq) { - std::lock_guard lock(m_); - assert(init_); + boost::optional shardIndex; - if (incomplete_) - return incomplete_->prepare(); - if (!canAdd_) - return boost::none; - if (backed_) { + std::lock_guard lock(mutex_); + assert(init_); + + if (acquireIndex_ != 0) + { + if (auto it {shards_.find(acquireIndex_)}; it != shards_.end()) + return it->second.shard->prepare(); + assert(false); + return boost::none; + } + + if (!canAdd_) + return boost::none; + // Check available storage space if (fileSz_ + avgShardFileSz_ > maxFileSz_) { @@ -266,39 +231,45 @@ DatabaseShardImp::prepareLedger(std::uint32_t validLedgerSeq) canAdd_ = false; return boost::none; } + + shardIndex = findAcquireIndex(validLedgerSeq, lock); } - auto const shardIndex {findShardIndexToAdd(validLedgerSeq, lock)}; if (!shardIndex) { JLOG(j_.debug()) << "no new shards to add"; - canAdd_ = false; + { + std::lock_guard lock(mutex_); + canAdd_ = false; + } return boost::none; } - // With every new shard, clear family caches - app_.shardFamily()->reset(); - incomplete_ = std::make_unique(app_, *this, *shardIndex, j_); - if (!incomplete_->open(scheduler_, *ctx_)) - { - incomplete_.reset(); + auto shard {std::make_unique(app_, *this, *shardIndex, j_)}; + if (!shard->open(scheduler_, *ctx_)) return boost::none; - } - return incomplete_->prepare(); + auto const seq {shard->prepare()}; + { + std::lock_guard lock(mutex_); + shards_.emplace( + *shardIndex, + ShardInfo(std::move(shard), ShardInfo::State::acquire)); + acquireIndex_ = *shardIndex; + } + return seq; } bool DatabaseShardImp::prepareShard(std::uint32_t shardIndex) { - std::lock_guard lock(m_); - assert(init_); - auto fail = [j = j_, shardIndex](std::string const& msg) { JLOG(j.error()) << "shard " << shardIndex << " " << msg; return false; }; + std::lock_guard lock(mutex_); + assert(init_); if (!canAdd_) return fail("cannot be stored at this time"); @@ -314,7 +285,7 @@ DatabaseShardImp::prepareShard(std::uint32_t shardIndex) auto seqCheck = [&](std::uint32_t seq) { // seq will be greater than zero if valid - if (seq > earliestSeq() && shardIndex >= seqToShardIndex(seq)) + if (seq > earliestLedgerSeq() && shardIndex >= seqToShardIndex(seq)) return fail("has an invalid index"); return true; }; @@ -324,50 +295,35 @@ DatabaseShardImp::prepareShard(std::uint32_t shardIndex) return false; } - if (complete_.find(shardIndex) != complete_.end()) - { - JLOG(j_.debug()) << "shard " << shardIndex << " is already stored"; - return false; - } - if (incomplete_ && incomplete_->index() == shardIndex) - { - JLOG(j_.debug()) << "shard " << shardIndex << " is being acquired"; - return false; - } - if (preShards_.find(shardIndex) != preShards_.end()) + if (shards_.find(shardIndex) != shards_.end()) { JLOG(j_.debug()) << - "shard " << shardIndex << " is already prepared for import"; + "shard " << shardIndex << + " is already stored or queued for import"; return false; } - // Check limit and space requirements - if (backed_) - { - std::uint64_t const sz { - (preShards_.size() + 1 + (incomplete_ ? 1 : 0)) * avgShardFileSz_}; - if (fileSz_ + sz > maxFileSz_) - { - JLOG(j_.debug()) << - "shard " << shardIndex << " exceeds the maximum storage size"; - return false; - } - if (sz > available()) - return fail("insufficient storage space available"); - } + // Check available storage space + if (fileSz_ + avgShardFileSz_ > maxFileSz_) + return fail("maximum storage size reached"); + if (avgShardFileSz_ > available()) + return fail("insufficient storage space available"); - // Add to shards prepared - preShards_.emplace(shardIndex, nullptr); + shards_.emplace(shardIndex, ShardInfo(nullptr, ShardInfo::State::import)); return true; } void DatabaseShardImp::removePreShard(std::uint32_t shardIndex) { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - preShards_.erase(shardIndex); + if (auto const it {shards_.find(shardIndex)}; + it != shards_.end() && it->second.state == ShardInfo::State::import) + { + shards_.erase(it); + } } std::string @@ -375,27 +331,32 @@ DatabaseShardImp::getPreShards() { RangeSet rs; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - if (preShards_.empty()) - return {}; - for (auto const& ps : preShards_) - rs.insert(ps.first); + for (auto const& e : shards_) + if (e.second.state == ShardInfo::State::import) + rs.insert(e.first); } + + if (rs.empty()) + return {}; + return to_string(rs); }; bool -DatabaseShardImp::importShard(std::uint32_t shardIndex, - boost::filesystem::path const& srcDir, bool validate) +DatabaseShardImp::importShard( + std::uint32_t shardIndex, + boost::filesystem::path const& srcDir) { using namespace boost::filesystem; try { if (!is_directory(srcDir) || is_empty(srcDir)) { - JLOG(j_.error()) << "invalid source directory " << srcDir.string(); + JLOG(j_.error()) << + "invalid source directory " << srcDir.string(); return false; } } @@ -406,7 +367,7 @@ DatabaseShardImp::importShard(std::uint32_t shardIndex, return false; } - auto move = [&](path const& src, path const& dst) + auto renameDir = [&](path const& src, path const& dst) { try { @@ -421,86 +382,88 @@ DatabaseShardImp::importShard(std::uint32_t shardIndex, return true; }; - std::unique_lock lock(m_); - assert(init_); - - // Check shard is prepared - auto it {preShards_.find(shardIndex)}; - if(it == preShards_.end()) + path dstDir; { - JLOG(j_.error()) << "shard " << shardIndex << " is an invalid index"; - return false; + std::lock_guard lock(mutex_); + assert(init_); + + // Check shard is prepared + if (auto const it {shards_.find(shardIndex)}; + it == shards_.end() || + it->second.shard || + it->second.state != ShardInfo::State::import) + { + JLOG(j_.error()) << + "shard " << shardIndex << " failed to import"; + return false; + } + + dstDir = dir_ / std::to_string(shardIndex); } - // Move source directory to the shard database directory - auto const dstDir {dir_ / std::to_string(shardIndex)}; - if (!move(srcDir, dstDir)) + // Rename source directory to the shard database directory + if (!renameDir(srcDir, dstDir)) return false; // Create the new shard auto shard {std::make_unique(app_, *this, shardIndex, j_)}; - auto fail = [&](std::string const& msg) + if (!shard->open(scheduler_, *ctx_) || !shard->isBackendComplete()) { - if (!msg.empty()) - { - JLOG(j_.error()) << "shard " << shardIndex << " " << msg; - } + JLOG(j_.error()) << + "shard " << shardIndex << " failed to import"; shard.reset(); - move(dstDir, srcDir); + renameDir(dstDir, srcDir); return false; - }; - - if (!shard->open(scheduler_, *ctx_)) - return fail({}); - if (!shard->complete()) - return fail("is incomplete"); - - try - { - // Verify database integrity - shard->getBackend()->verify(); - } - catch (std::exception const& e) - { - return fail(std::string("exception ") + - e.what() + " in function " + __func__); } - // Validate shard ledgers - if (validate) + std::lock_guard lock(mutex_); + auto const it {shards_.find(shardIndex)}; + if (it == shards_.end() || + it->second.shard || + it->second.state != ShardInfo::State::import) { - // Shard validation requires releasing the lock - // so the database can fetch data from it - it->second = shard.get(); - lock.unlock(); - auto const valid {shard->validate()}; - lock.lock(); - if (!valid) - { - it = preShards_.find(shardIndex); - if(it != preShards_.end()) - it->second = nullptr; - return fail("failed validation"); - } + JLOG(j_.error()) << + "shard " << shardIndex << " failed to import"; + return false; } - // Add the shard - complete_.emplace(shardIndex, std::move(shard)); - preShards_.erase(shardIndex); - - std::lock_guard lockg(*lock.release(), std::adopt_lock); - setFileStats(lockg); - updateStatus(lockg); + it->second.shard = std::move(shard); + finalizeShard(it->second, true, lock); return true; } std::shared_ptr DatabaseShardImp::fetchLedger(uint256 const& hash, std::uint32_t seq) { - if (!contains(seq)) - return {}; + auto const shardIndex {seqToShardIndex(seq)}; + { + ShardInfo shardInfo; + { + std::lock_guard lock(mutex_); + assert(init_); + + if (auto const it {shards_.find(shardIndex)}; it != shards_.end()) + shardInfo = it->second; + else + return {}; + } + + // Check if the ledger is stored in a final shard + // or in the shard being acquired + switch (shardInfo.state) + { + case ShardInfo::State::final: + break; + case ShardInfo::State::acquire: + if (shardInfo.shard->containsLedger(seq)) + break; + [[fallthrough]]; + default: + return {}; + } + } - auto nObj = fetch(hash, seq); + auto nObj {fetch(hash, seq)}; if (!nObj) return {}; @@ -549,69 +512,63 @@ DatabaseShardImp::fetchLedger(uint256 const& hash, std::uint32_t seq) void DatabaseShardImp::setStored(std::shared_ptr const& ledger) { - auto const shardIndex {seqToShardIndex(ledger->info().seq)}; - auto fail = [j = j_, shardIndex](std::string const& msg) - { - JLOG(j.error()) << "shard " << shardIndex << " " << msg; - }; - if (ledger->info().hash.isZero()) { - return fail("encountered a zero ledger hash on sequence " + - std::to_string(ledger->info().seq)); + JLOG(j_.error()) << + "zero ledger hash for ledger sequence " << ledger->info().seq; + return; } if (ledger->info().accountHash.isZero()) { - return fail("encountered a zero account hash on sequence " + - std::to_string(ledger->info().seq)); + JLOG(j_.error()) << + "zero account hash for ledger sequence " << ledger->info().seq; + return; } - - std::lock_guard lock(m_); - assert(init_); - - if (!incomplete_ || shardIndex != incomplete_->index()) + if (ledger->stateMap().getHash().isNonZero() && + !ledger->stateMap().isValid()) { - return fail("ledger sequence " + std::to_string(ledger->info().seq) + - " is not being acquired"); - } - if (!incomplete_->setStored(ledger)) + JLOG(j_.error()) << + "invalid state map for ledger sequence " << ledger->info().seq; return; - if (incomplete_->complete()) + } + if (ledger->info().txHash.isNonZero() && !ledger->txMap().isValid()) { - complete_.emplace(incomplete_->index(), std::move(incomplete_)); - incomplete_.reset(); - updateStatus(lock); - - // Update peers with new shard index - protocol::TMPeerShardInfo message; - PublicKey const& publicKey {app_.nodeIdentity().first}; - message.set_nodepubkey(publicKey.data(), publicKey.size()); - message.set_shardindexes(std::to_string(shardIndex)); - app_.overlay().foreach(send_always( - std::make_shared(message, protocol::mtPEER_SHARD_INFO))); + JLOG(j_.error()) << + "invalid transaction map for ledger sequence " << + ledger->info().seq; + return; } - setFileStats(lock); -} + auto const shardIndex {seqToShardIndex(ledger->info().seq)}; + std::shared_ptr shard; + { + std::lock_guard lock(mutex_); + assert(init_); -bool -DatabaseShardImp::contains(std::uint32_t seq) -{ - auto const shardIndex {seqToShardIndex(seq)}; - std::lock_guard lock(m_); - assert(init_); + if (shardIndex != acquireIndex_) + { + JLOG(j_.trace()) << + "shard " << shardIndex << " is not being acquired"; + return; + } - if (complete_.find(shardIndex) != complete_.end()) - return true; - if (incomplete_ && incomplete_->index() == shardIndex) - return incomplete_->contains(seq); - return false; + if (auto const it {shards_.find(shardIndex)}; it != shards_.end()) + shard = it->second.shard; + else + { + JLOG(j_.error()) << + "shard " << shardIndex << " is not being acquired"; + return; + } + } + + storeLedgerInShard(shard, ledger); } std::string DatabaseShardImp::getCompleteShards() { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); return status_; @@ -620,36 +577,53 @@ DatabaseShardImp::getCompleteShards() void DatabaseShardImp::validate() { - std::vector> completeShards; + std::vector> shards; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - if (complete_.empty()) - { - JLOG(j_.error()) << "no shards found to validate"; + // Only shards with a state of final should be validated + for (auto& e : shards_) + if (e.second.state == ShardInfo::State::final) + shards.push_back(e.second.shard); + + if (shards.empty()) return; - } JLOG(j_.debug()) << "Validating shards " << status_; - - completeShards.reserve(complete_.size()); - for (auto const& shard : complete_) - completeShards.push_back(shard.second); } - // Verify each complete stored shard - for (auto const& shard : completeShards) - shard->validate(); + for (auto const& e : shards) + { + if (auto shard {e.lock()}; shard) + shard->finalize(true); + } app_.shardFamily()->reset(); } +void +DatabaseShardImp::onStop() +{ + // Stop read threads in base before data members are destroyed + stopThreads(); + + std::lock_guard lock(mutex_); + if (shards_.empty()) + return; + + // Notify shards to stop + for (auto const& e : shards_) + if (e.second.shard) + e.second.shard->stop(); + shards_.clear(); +} + void DatabaseShardImp::import(Database& source) { { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); // Only the application local node store can be imported @@ -669,7 +643,7 @@ DatabaseShardImp::import(Database& source) std::shared_ptr ledger; std::uint32_t seq; std::tie(ledger, seq, std::ignore) = loadLedgerHelper( - "WHERE LedgerSeq >= " + std::to_string(earliestSeq()) + + "WHERE LedgerSeq >= " + std::to_string(earliestLedgerSeq()) + " order by LedgerSeq " + (ascendSort ? "asc" : "desc") + " limit 1", app_, false); if (!ledger || seq == 0) @@ -729,10 +703,11 @@ DatabaseShardImp::import(Database& source) } // Skip if already stored - if (complete_.find(shardIndex) != complete_.end() || - (incomplete_ && incomplete_->index() == shardIndex)) + if (shardIndex == acquireIndex_ || + shards_.find(shardIndex) != shards_.end()) { - JLOG(j_.debug()) << "shard " << shardIndex << " already exists"; + JLOG(j_.debug()) << + "shard " << shardIndex << " already exists"; continue; } @@ -743,7 +718,7 @@ DatabaseShardImp::import(Database& source) std::max(firstSeq, lastLedgerSeq(shardIndex))}; auto const numLedgers {shardIndex == earliestShardIndex() ? lastSeq - firstSeq + 1 : ledgersPerShard_}; - auto ledgerHashes{getHashesByIndex(firstSeq, lastSeq, app_)}; + auto ledgerHashes {getHashesByIndex(firstSeq, lastSeq, app_)}; if (ledgerHashes.size() != numLedgers) continue; @@ -768,126 +743,178 @@ DatabaseShardImp::import(Database& source) auto const shardDir {dir_ / std::to_string(shardIndex)}; auto shard {std::make_unique(app_, *this, shardIndex, j_)}; if (!shard->open(scheduler_, *ctx_)) - { - shard.reset(); continue; - } // Create a marker file to signify an import in progress auto const markerFile {shardDir / importMarker_}; - std::ofstream ofs {markerFile.string()}; - if (!ofs.is_open()) { - JLOG(j_.error()) << - "shard " << shardIndex << - " is unable to create temp marker file"; - shard.reset(); - removeAll(shardDir, j_); - continue; + std::ofstream ofs {markerFile.string()}; + if (!ofs.is_open()) + { + JLOG(j_.error()) << + "shard " << shardIndex << + " is unable to create temp marker file"; + remove_all(shardDir); + continue; + } + ofs.close(); } - ofs.close(); // Copy the ledgers from node store + std::shared_ptr recentStored; + boost::optional lastLedgerHash; + while (auto seq = shard->prepare()) { - auto ledger = loadByIndex(*seq, app_, false); - if (!ledger || ledger->info().seq != seq || - !Database::copyLedger(*shard->getBackend(), *ledger, - nullptr, nullptr, shard->lastStored())) + auto ledger {loadByIndex(*seq, app_, false)}; + if (!ledger || ledger->info().seq != seq) break; - if (!shard->setStored(ledger)) - break; - if (shard->complete()) + if (!Database::storeLedger( + *ledger, + shard->getBackend(), + nullptr, + nullptr, + recentStored)) { - JLOG(j_.debug()) << - "shard " << shardIndex << " was successfully imported"; - removeAll(markerFile, j_); break; } + + if (!shard->store(ledger)) + break; + + if (!lastLedgerHash && seq == lastLedgerSeq(shardIndex)) + lastLedgerHash = ledger->info().hash; + + recentStored = ledger; } - if (!shard->complete()) + using namespace boost::filesystem; + if (lastLedgerHash && shard->isBackendComplete()) + { + // Store shard final key + Serializer s; + s.add32(Shard::version); + s.add32(firstLedgerSeq(shardIndex)); + s.add32(lastLedgerSeq(shardIndex)); + s.add256(*lastLedgerHash); + auto nObj {NodeObject::createObject( + hotUNKNOWN, + std::move(s.modData()), + Shard::finalKey)}; + + try + { + shard->getBackend()->store(nObj); + + // The import process is complete and the + // marker file is no longer required + remove_all(markerFile); + + JLOG(j_.debug()) << + "shard " << shardIndex << + " was successfully imported"; + + auto const result {shards_.emplace( + shardIndex, + ShardInfo(std::move(shard), ShardInfo::State::none))}; + finalizeShard(result.first->second, true, lock); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << + "exception " << e.what() << + " in function " << __func__; + remove_all(shardDir); + } + } + else { JLOG(j_.error()) << "shard " << shardIndex << " failed to import"; - shard.reset(); - removeAll(shardDir, j_); + remove_all(shardDir); } - else - setFileStats(lock); } - // Re initialize the shard store - init_ = false; - complete_.clear(); - incomplete_.reset(); + updateStatus(lock); } - if (!init()) - Throw("import: failed to initialize"); + setFileStats(); } std::int32_t DatabaseShardImp::getWriteLoad() const { - std::int32_t wl {0}; + std::shared_ptr shard; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - for (auto const& e : complete_) - wl += e.second->getBackend()->getWriteLoad(); - if (incomplete_) - wl += incomplete_->getBackend()->getWriteLoad(); + if (auto const it {shards_.find(acquireIndex_)}; it != shards_.end()) + shard = it->second.shard; + else + return 0; } - return wl; + + return shard->getBackend()->getWriteLoad(); } void -DatabaseShardImp::store(NodeObjectType type, - Blob&& data, uint256 const& hash, std::uint32_t seq) +DatabaseShardImp::store( + NodeObjectType type, + Blob&& data, + uint256 const& hash, + std::uint32_t seq) { -#if RIPPLE_VERIFY_NODEOBJECT_KEYS - assert(hash == sha512Hash(makeSlice(data))); -#endif - std::shared_ptr nObj; auto const shardIndex {seqToShardIndex(seq)}; + std::shared_ptr shard; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - if (!incomplete_ || shardIndex != incomplete_->index()) + if (shardIndex != acquireIndex_) + { + JLOG(j_.trace()) << + "shard " << shardIndex << " is not being acquired"; + return; + } + + if (auto const it {shards_.find(shardIndex)}; it != shards_.end()) + shard = it->second.shard; + else { - JLOG(j_.warn()) << - "shard " << shardIndex << - " ledger sequence " << seq << - " is not being acquired"; + JLOG(j_.error()) << + "shard " << shardIndex << " is not being acquired"; return; } - nObj = NodeObject::createObject( - type, std::move(data), hash); - incomplete_->pCache()->canonicalize(hash, nObj, true); - incomplete_->getBackend()->store(nObj); - incomplete_->nCache()->erase(hash); } + + auto [backend, pCache, nCache] = shard->getBackendAll(); + auto nObj {NodeObject::createObject(type, std::move(data), hash)}; + + pCache->canonicalize(hash, nObj, true); + backend->store(nObj); + nCache->erase(hash); + storeStats(nObj->getData().size()); } std::shared_ptr DatabaseShardImp::fetch(uint256 const& hash, std::uint32_t seq) { - auto cache {selectCache(seq)}; + auto cache {getCache(seq)}; if (cache.first) return doFetch(hash, seq, *cache.first, *cache.second, false); return {}; } bool -DatabaseShardImp::asyncFetch(uint256 const& hash, - std::uint32_t seq, std::shared_ptr& object) +DatabaseShardImp::asyncFetch( + uint256 const& hash, + std::uint32_t seq, + std::shared_ptr& object) { - auto cache {selectCache(seq)}; + auto cache {getCache(seq)}; if (cache.first) { // See if the object is in cache @@ -901,125 +928,227 @@ DatabaseShardImp::asyncFetch(uint256 const& hash, } bool -DatabaseShardImp::copyLedger(std::shared_ptr const& ledger) +DatabaseShardImp::storeLedger(std::shared_ptr const& srcLedger) { - auto const shardIndex {seqToShardIndex(ledger->info().seq)}; - std::lock_guard lock(m_); - assert(init_); - - if (!incomplete_ || shardIndex != incomplete_->index()) + auto const seq {srcLedger->info().seq}; + auto const shardIndex {seqToShardIndex(seq)}; + std::shared_ptr shard; { - JLOG(j_.warn()) << - "shard " << shardIndex << - " source ledger sequence " << ledger->info().seq << - " is not being acquired"; - return false; + std::lock_guard lock(mutex_); + assert(init_); + + if (shardIndex != acquireIndex_) + { + JLOG(j_.trace()) << + "shard " << shardIndex << " is not being acquired"; + return false; + } + + if (auto const it {shards_.find(shardIndex)}; it != shards_.end()) + shard = it->second.shard; + else + { + JLOG(j_.error()) << + "shard " << shardIndex << " is not being acquired"; + return false; + } } - if (!Database::copyLedger(*incomplete_->getBackend(), *ledger, - incomplete_->pCache(), incomplete_->nCache(), - incomplete_->lastStored())) + if (shard->containsLedger(seq)) { + JLOG(j_.trace()) << + "shard " << shardIndex << " ledger already stored"; return false; } - if (!incomplete_->setStored(ledger)) - return false; - if (incomplete_->complete()) { - complete_.emplace(incomplete_->index(), std::move(incomplete_)); - incomplete_.reset(); - updateStatus(lock); + auto [backend, pCache, nCache] = shard->getBackendAll(); + if (!Database::storeLedger( + *srcLedger, + backend, + pCache, + nCache, + nullptr)) + { + return false; + } } - setFileStats(lock); - return true; + return storeLedgerInShard(shard, srcLedger); } int DatabaseShardImp::getDesiredAsyncReadCount(std::uint32_t seq) { auto const shardIndex {seqToShardIndex(seq)}; + std::shared_ptr shard; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - auto it = complete_.find(shardIndex); - if (it != complete_.end()) - return it->second->pCache()->getTargetSize() / asyncDivider; - if (incomplete_ && incomplete_->index() == shardIndex) - return incomplete_->pCache()->getTargetSize() / asyncDivider; + if (auto const it {shards_.find(shardIndex)}; + it != shards_.end() && + (it->second.state == ShardInfo::State::final || + it->second.state == ShardInfo::State::acquire)) + { + shard = it->second.shard; + } + else + return 0; } - return cacheTargetSize / asyncDivider; + + return shard->pCache()->getTargetSize() / asyncDivider; } float DatabaseShardImp::getCacheHitRate() { - float sz, f {0}; + std::shared_ptr shard; { - std::lock_guard lock(m_); + std::lock_guard lock(mutex_); assert(init_); - sz = complete_.size(); - for (auto const& e : complete_) - f += e.second->pCache()->getHitRate(); - if (incomplete_) - { - f += incomplete_->pCache()->getHitRate(); - ++sz; - } + if (auto const it {shards_.find(acquireIndex_)}; it != shards_.end()) + shard = it->second.shard; + else + return 0; } - return f / std::max(1.0f, sz); + + return shard->pCache()->getHitRate(); } void DatabaseShardImp::sweep() { - std::lock_guard lock(m_); - assert(init_); + std::vector> shards; + { + std::lock_guard lock(mutex_); + assert(init_); - for (auto const& e : complete_) - e.second->sweep(); + for (auto const& e : shards_) + if (e.second.state == ShardInfo::State::final || + e.second.state == ShardInfo::State::acquire) + { + shards.push_back(e.second.shard); + } + } - if (incomplete_) - incomplete_->sweep(); + for (auto const& e : shards) + { + if (auto shard {e.lock()}; shard) + shard->sweep(); + } } -std::shared_ptr -DatabaseShardImp::fetchFrom(uint256 const& hash, std::uint32_t seq) +bool +DatabaseShardImp::initConfig(std::lock_guard&) { - auto const shardIndex {seqToShardIndex(seq)}; - std::unique_lock lock(m_); - assert(init_); + auto fail = [j = j_](std::string const& msg) + { + JLOG(j.error()) << + "[" << ConfigSection::shardDatabase() << "] " << msg; + return false; + }; + + Config const& config {app_.config()}; + Section const& section {config.section(ConfigSection::shardDatabase())}; + { - auto it = complete_.find(shardIndex); - if (it != complete_.end()) + // The earliest ledger sequence defaults to XRP_LEDGER_EARLIEST_SEQ. + // A custom earliest ledger sequence can be set through the + // configuration file using the 'earliest_seq' field under the + // 'node_db' and 'shard_db' stanzas. If specified, this field must + // have a value greater than zero and be equally assigned in + // both stanzas. + + std::uint32_t shardDBEarliestSeq {0}; + get_if_exists( + section, + "earliest_seq", + shardDBEarliestSeq); + + std::uint32_t nodeDBEarliestSeq {0}; + get_if_exists( + config.section(ConfigSection::nodeDatabase()), + "earliest_seq", + nodeDBEarliestSeq); + + if (shardDBEarliestSeq != nodeDBEarliestSeq) { - lock.unlock(); - return fetchInternal(hash, *it->second->getBackend()); + return fail("and [" + ConfigSection::nodeDatabase() + + "] define different 'earliest_seq' values"); } } - if (incomplete_ && incomplete_->index() == shardIndex) + + using namespace boost::filesystem; + if (!get_if_exists(section, "path", dir_)) + return fail("'path' missing"); + { - lock.unlock(); - return fetchInternal(hash, *incomplete_->getBackend()); + std::uint64_t sz; + if (!get_if_exists(section, "max_size_gb", sz)) + return fail("'max_size_gb' missing"); + + if ((sz << 30) < sz) + return fail("'max_size_gb' overflow"); + + // Minimum storage space required (in gigabytes) + if (sz < 10) + return fail("'max_size_gb' must be at least 10"); + + // Convert to bytes + maxFileSz_ = sz << 30; } - // Used to validate import shards - auto it = preShards_.find(shardIndex); - if (it != preShards_.end() && it->second) + if (section.exists("ledgers_per_shard")) { - lock.unlock(); - return fetchInternal(hash, *it->second->getBackend()); + // To be set only in standalone for testing + if (!config.standalone()) + return fail("'ledgers_per_shard' only honored in stand alone"); + + ledgersPerShard_ = get(section, "ledgers_per_shard"); + if (ledgersPerShard_ == 0 || ledgersPerShard_ % 256 != 0) + return fail("'ledgers_per_shard' must be a multiple of 256"); } - return {}; + + // NuDB is the default and only supported permanent storage backend + backendName_ = get(section, "type", "nudb"); + if (!boost::iequals(backendName_, "NuDB")) + return fail("'type' value unsupported"); + + return true; +} + +std::shared_ptr +DatabaseShardImp::fetchFrom(uint256 const& hash, std::uint32_t seq) +{ + auto const shardIndex {seqToShardIndex(seq)}; + std::shared_ptr shard; + { + std::lock_guard lock(mutex_); + assert(init_); + + if (auto const it {shards_.find(shardIndex)}; + it != shards_.end() && + it->second.shard) + { + shard = it->second.shard; + } + else + return {}; + } + + return fetchInternal(hash, shard->getBackend()); } boost::optional -DatabaseShardImp::findShardIndexToAdd( - std::uint32_t validLedgerSeq, std::lock_guard&) +DatabaseShardImp::findAcquireIndex( + std::uint32_t validLedgerSeq, + std::lock_guard&) { + if (validLedgerSeq < earliestLedgerSeq()) + return boost::none; + auto const maxShardIndex {[this, validLedgerSeq]() { auto shardIndex {seqToShardIndex(validLedgerSeq)}; @@ -1027,31 +1156,26 @@ DatabaseShardImp::findShardIndexToAdd( --shardIndex; return shardIndex; }()}; - auto const numShards {complete_.size() + - (incomplete_ ? 1 : 0) + preShards_.size()}; + auto const maxNumShards {maxShardIndex - earliestShardIndex() + 1}; // Check if the shard store has all shards - if (numShards >= maxShardIndex) + if (shards_.size() >= maxNumShards) return boost::none; if (maxShardIndex < 1024 || - static_cast(numShards) / maxShardIndex > 0.5f) + static_cast(shards_.size()) / maxNumShards > 0.5f) { // Small or mostly full index space to sample // Find the available indexes and select one at random std::vector available; - available.reserve(maxShardIndex - numShards + 1); + available.reserve(maxNumShards - shards_.size()); for (auto shardIndex = earliestShardIndex(); shardIndex <= maxShardIndex; ++shardIndex) { - if (complete_.find(shardIndex) == complete_.end() && - (!incomplete_ || incomplete_->index() != shardIndex) && - preShards_.find(shardIndex) == preShards_.end()) - { + if (shards_.find(shardIndex) == shards_.end()) available.push_back(shardIndex); - } } if (available.empty()) @@ -1070,12 +1194,8 @@ DatabaseShardImp::findShardIndexToAdd( for (int i = 0; i < 40; ++i) { auto const shardIndex {rand_int(earliestShardIndex(), maxShardIndex)}; - if (complete_.find(shardIndex) == complete_.end() && - (!incomplete_ || incomplete_->index() != shardIndex) && - preShards_.find(shardIndex) == preShards_.end()) - { + if (shards_.find(shardIndex) == shards_.end()) return shardIndex; - } } assert(false); @@ -1083,33 +1203,130 @@ DatabaseShardImp::findShardIndexToAdd( } void -DatabaseShardImp::setFileStats(std::lock_guard&) +DatabaseShardImp::finalizeShard( + ShardInfo& shardInfo, + bool writeSQLite, + std::lock_guard&) { - fileSz_ = 0; - fdRequired_ = 0; - if (!complete_.empty()) + assert(shardInfo.shard); + assert(shardInfo.shard->index() != acquireIndex_); + assert(shardInfo.shard->isBackendComplete()); + assert(shardInfo.state != ShardInfo::State::finalize); + + auto const shardIndex {shardInfo.shard->index()}; + + shardInfo.state = ShardInfo::State::finalize; + taskQueue_->addTask([this, shardIndex, writeSQLite]() { - for (auto const& e : complete_) + if (isStopping()) + return; + + std::shared_ptr shard; { - fileSz_ += e.second->fileSize(); - fdRequired_ += e.second->fdRequired(); + std::lock_guard lock(mutex_); + if (auto const it {shards_.find(shardIndex)}; it != shards_.end()) + shard = it->second.shard; + else + { + JLOG(j_.error()) << + "Unable to finalize shard " << shardIndex; + return; + } } - avgShardFileSz_ = fileSz_ / complete_.size(); - } - else - avgShardFileSz_ = 0; - if (incomplete_) + if (!shard->finalize(writeSQLite)) + { + if (isStopping()) + return; + + // Bad shard, remove it + { + std::lock_guard lock(mutex_); + shards_.erase(shardIndex); + updateStatus(lock); + + using namespace boost::filesystem; + path const dir {shard->getDir()}; + shard.reset(); + try + { + remove_all(dir); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << + "exception " << e.what() << " in function " << __func__; + } + } + + setFileStats(); + return; + } + + if (isStopping()) + return; + + { + std::lock_guard lock(mutex_); + auto const it {shards_.find(shardIndex)}; + if (it == shards_.end()) + return; + it->second.state = ShardInfo::State::final; + updateStatus(lock); + } + + setFileStats(); + + // Update peers with new shard index + if (!app_.config().standalone() && + app_.getOPs().getOperatingMode() != OperatingMode::DISCONNECTED) + { + protocol::TMPeerShardInfo message; + PublicKey const& publicKey {app_.nodeIdentity().first}; + message.set_nodepubkey(publicKey.data(), publicKey.size()); + message.set_shardindexes(std::to_string(shardIndex)); + app_.overlay().foreach(send_always( + std::make_shared( + message, + protocol::mtPEER_SHARD_INFO))); + } + }); +} + +void +DatabaseShardImp::setFileStats() +{ + std::vector> shards; { - fileSz_ += incomplete_->fileSize(); - fdRequired_ += incomplete_->fdRequired(); + std::lock_guard lock(mutex_); + assert(init_); + + if (shards_.empty()) + return; + + for (auto const& e : shards_) + if (e.second.shard) + shards.push_back(e.second.shard); } - if (!backed_) - return; + std::uint64_t sumSz {0}; + std::uint32_t sumFd {0}; + std::uint32_t numShards {0}; + for (auto const& e : shards) + { + if (auto shard {e.lock()}; shard) + { + auto[sz, fd] = shard->fileInfo(); + sumSz += sz; + sumFd += fd; + ++numShards; + } + } - // Require at least 15 file descriptors - fdRequired_ = std::max(fdRequired_, 15); + std::lock_guard lock(mutex_); + fileSz_ = sumSz; + fdRequired_ = sumFd; + avgShardFileSz_ = fileSz_ / numShards; if (fileSz_ >= maxFileSz_) { @@ -1126,11 +1343,12 @@ DatabaseShardImp::setFileStats(std::lock_guard&) void DatabaseShardImp::updateStatus(std::lock_guard&) { - if (!complete_.empty()) + if (!shards_.empty()) { RangeSet rs; - for (auto const& e : complete_) - rs.insert(e.second->index()); + for (auto const& e : shards_) + if (e.second.state == ShardInfo::State::final) + rs.insert(e.second.shard->index()); status_ = to_string(rs); } else @@ -1138,32 +1356,28 @@ DatabaseShardImp::updateStatus(std::lock_guard&) } std::pair, std::shared_ptr> -DatabaseShardImp::selectCache(std::uint32_t seq) +DatabaseShardImp::getCache(std::uint32_t seq) { auto const shardIndex {seqToShardIndex(seq)}; - std::lock_guard lock(m_); - assert(init_); - + std::shared_ptr shard; { - auto it = complete_.find(shardIndex); - if (it != complete_.end()) + std::lock_guard lock(mutex_); + assert(init_); + + if (auto const it {shards_.find(shardIndex)}; + it != shards_.end() && it->second.shard) { - return std::make_pair(it->second->pCache(), - it->second->nCache()); + shard = it->second.shard; } + else + return {}; } - if (incomplete_ && incomplete_->index() == shardIndex) - { - return std::make_pair(incomplete_->pCache(), - incomplete_->nCache()); - } + std::shared_ptr pCache; + std::shared_ptr nCache; + std::tie(std::ignore, pCache, nCache) = shard->getBackendAll(); - // Used to validate import shards - auto it = preShards_.find(shardIndex); - if (it != preShards_.end() && it->second) - return std::make_pair(it->second->pCache(), it->second->nCache()); - return {}; + return std::make_pair(pCache, nCache); } std::uint64_t @@ -1175,12 +1389,70 @@ DatabaseShardImp::available() const } catch (std::exception const& e) { - JLOG(j_.error()) << "exception " << e.what() << - " in function " << __func__; + JLOG(j_.error()) << + "exception " << e.what() << " in function " << __func__; return 0; } } +bool +DatabaseShardImp::storeLedgerInShard( + std::shared_ptr& shard, + std::shared_ptr const& ledger) +{ + bool result {true}; + + if (!shard->store(ledger)) + { + // Shard may be corrupt, remove it + std::lock_guard lock(mutex_); + + shards_.erase(shard->index()); + if (shard->index() == acquireIndex_) + acquireIndex_ = 0; + + updateStatus(lock); + + using namespace boost::filesystem; + path const dir {shard->getDir()}; + shard.reset(); + try + { + remove_all(dir); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << + "exception " << e.what() << " in function " << __func__; + } + + result = false; + } + else if (shard->isBackendComplete()) + { + std::lock_guard lock(mutex_); + + if (auto const it {shards_.find(shard->index())}; + it != shards_.end()) + { + if (shard->index() == acquireIndex_) + acquireIndex_ = 0; + + if (it->second.state != ShardInfo::State::finalize) + finalizeShard(it->second, false, lock); + } + else + { + JLOG(j_.debug()) << + "shard " << shard->index() << + " is no longer being acquired"; + } + } + + setFileStats(); + return result; +} + //------------------------------------------------------------------------------ std::unique_ptr @@ -1197,19 +1469,13 @@ make_ShardStore( if (section.empty()) return nullptr; - auto shardStore = std::make_unique( + return std::make_unique( app, parent, "ShardStore", scheduler, readThreads, j); - if (shardStore->init()) - shardStore->setParent(parent); - else - shardStore.reset(); - - return shardStore; } } // NodeStore diff --git a/src/ripple/nodestore/impl/DatabaseShardImp.h b/src/ripple/nodestore/impl/DatabaseShardImp.h index 9d90e4d5baa..21a117bdab1 100644 --- a/src/ripple/nodestore/impl/DatabaseShardImp.h +++ b/src/ripple/nodestore/impl/DatabaseShardImp.h @@ -22,6 +22,7 @@ #include #include +#include namespace ripple { namespace NodeStore { @@ -61,8 +62,9 @@ class DatabaseShardImp : public DatabaseShard getPreShards() override; bool - importShard(std::uint32_t shardIndex, - boost::filesystem::path const& srcDir, bool validate) override; + importShard( + std::uint32_t shardIndex, + boost::filesystem::path const& srcDir) override; std::shared_ptr fetchLedger(uint256 const& hash, std::uint32_t seq) override; @@ -70,9 +72,6 @@ class DatabaseShardImp : public DatabaseShard void setStored(std::shared_ptr const& ledger) override; - bool - contains(std::uint32_t seq) override; - std::string getCompleteShards() override; @@ -94,7 +93,7 @@ class DatabaseShardImp : public DatabaseShard std::uint32_t seqToShardIndex(std::uint32_t seq) const override { - assert(seq >= earliestSeq()); + assert(seq >= earliestLedgerSeq()); return NodeStore::seqToShardIndex(seq, ledgersPerShard_); } @@ -103,7 +102,7 @@ class DatabaseShardImp : public DatabaseShard { assert(shardIndex >= earliestShardIndex_); if (shardIndex <= earliestShardIndex_) - return earliestSeq(); + return earliestLedgerSeq(); return 1 + (shardIndex * ledgersPerShard_); } @@ -126,6 +125,9 @@ class DatabaseShardImp : public DatabaseShard return backendName_; } + void + onStop() override; + /** Import the application local node store @param source The application node store. @@ -137,18 +139,23 @@ class DatabaseShardImp : public DatabaseShard getWriteLoad() const override; void - store(NodeObjectType type, Blob&& data, - uint256 const& hash, std::uint32_t seq) override; + store( + NodeObjectType type, + Blob&& data, + uint256 const& hash, + std::uint32_t seq) override; std::shared_ptr fetch(uint256 const& hash, std::uint32_t seq) override; bool - asyncFetch(uint256 const& hash, std::uint32_t seq, + asyncFetch( + uint256 const& hash, + std::uint32_t seq, std::shared_ptr& object) override; bool - copyLedger(std::shared_ptr const& ledger) override; + storeLedger(std::shared_ptr const& srcLedger) override; int getDesiredAsyncReadCount(std::uint32_t seq) override; @@ -163,21 +170,43 @@ class DatabaseShardImp : public DatabaseShard sweep() override; private: + struct ShardInfo + { + enum class State + { + none, + final, // Immutable, complete and validated + acquire, // Being acquired + import, // Being imported + finalize // Being finalized + }; + + ShardInfo() = default; + ShardInfo(std::shared_ptr shard_, State state_) + : shard(std::move(shard_)) + , state(state_) + {} + + std::shared_ptr shard; + State state {State::none}; + }; + Application& app_; - mutable std::mutex m_; + Stoppable& parent_; + mutable std::mutex mutex_; bool init_ {false}; // The context shared with all shard backend databases std::unique_ptr ctx_; - // Complete shards - std::map> complete_; + // Queue of background tasks to be performed + std::unique_ptr taskQueue_; - // A shard being acquired from the peer network - std::unique_ptr incomplete_; + // Shards held by this server + std::map shards_; - // Shards prepared for import - std::map preShards_; + // Shard index being acquired from the peer network + std::uint32_t acquireIndex_ {0}; // The shard store root directory boost::filesystem::path dir_; @@ -188,9 +217,6 @@ class DatabaseShardImp : public DatabaseShard // Complete shard indexes std::string status_; - // If backend type uses permanent storage - bool backed_; - // The name associated with the backend used with the shard store std::string backendName_; @@ -214,6 +240,11 @@ class DatabaseShardImp : public DatabaseShard // File name used to mark shards being imported from node store static constexpr auto importMarker_ = "import"; + // Initialize settings from the configuration file + // Lock must be held + bool + initConfig(std::lock_guard&); + std::shared_ptr fetchFrom(uint256 const& hash, std::uint32_t seq) override; @@ -223,17 +254,25 @@ class DatabaseShardImp : public DatabaseShard Throw("Shard store import not supported"); } - // Finds a random shard index that is not stored + // Randomly select a shard index not stored // Lock must be held boost::optional - findShardIndexToAdd( + findAcquireIndex( std::uint32_t validLedgerSeq, std::lock_guard&); - // Set storage and file descriptor usage stats + // Queue a task to finalize a shard by validating its databases // Lock must be held void - setFileStats(std::lock_guard&); + finalizeShard( + ShardInfo& shardInfo, + bool writeSQLite, + std::lock_guard&); + + // Set storage and file descriptor usage stats + // Lock must NOT be held + void + setFileStats(); // Update status string // Lock must be held @@ -241,11 +280,16 @@ class DatabaseShardImp : public DatabaseShard updateStatus(std::lock_guard&); std::pair, std::shared_ptr> - selectCache(std::uint32_t seq); + getCache(std::uint32_t seq); // Returns available storage space std::uint64_t available() const; + + bool + storeLedgerInShard( + std::shared_ptr& shard, + std::shared_ptr const& ledger); }; } // NodeStore diff --git a/src/ripple/nodestore/impl/NodeObject.cpp b/src/ripple/nodestore/impl/NodeObject.cpp index 91a8459263e..682b3b3b4de 100644 --- a/src/ripple/nodestore/impl/NodeObject.cpp +++ b/src/ripple/nodestore/impl/NodeObject.cpp @@ -31,8 +31,8 @@ NodeObject::NodeObject ( PrivateAccess) : mType (type) , mHash (hash) + , mData (std::move(data)) { - mData = std::move (data); } std::shared_ptr diff --git a/src/ripple/nodestore/impl/Shard.cpp b/src/ripple/nodestore/impl/Shard.cpp index 2b685a661c7..086e4e4c37c 100644 --- a/src/ripple/nodestore/impl/Shard.cpp +++ b/src/ripple/nodestore/impl/Shard.cpp @@ -24,17 +24,16 @@ #include #include #include +#include #include -#include -#include #include -#include - namespace ripple { namespace NodeStore { +uint256 const Shard::finalKey {0}; + Shard::Shard( Application& app, DatabaseShard const& db, @@ -47,7 +46,6 @@ Shard::Shard( , maxLedgers_(index == db.earliestShardIndex() ? lastSeq_ - firstSeq_ + 1 : db.ledgersPerShard()) , dir_(db.getRootDir() / std::to_string(index_)) - , control_(dir_ / controlFileName) , j_(j) { if (index_ < db.earliestShardIndex()) @@ -57,96 +55,149 @@ Shard::Shard( bool Shard::open(Scheduler& scheduler, nudb::context& ctx) { - using namespace boost::filesystem; - std::lock_guard lock(mutex_); + std::lock_guard lock {mutex_}; assert(!backend_); Config const& config {app_.config()}; - Section section {config.section(ConfigSection::shardDatabase())}; - std::string const type (get(section, "type", "nudb")); - auto factory {Manager::instance().find(type)}; - if (!factory) { - JLOG(j_.error()) << - "shard " << index_ << - " failed to create backend type " << type; - return false; - } + Section section {config.section(ConfigSection::shardDatabase())}; + std::string const type {get(section, "type", "nudb")}; + auto factory {Manager::instance().find(type)}; + if (!factory) + { + JLOG(j_.error()) << + "shard " << index_ << + " failed to create backend type " << type; + return false; + } - section.set("path", dir_.string()); - backend_ = factory->createInstance( - NodeObject::keyBytes, section, scheduler, ctx, j_); + section.set("path", dir_.string()); + backend_ = factory->createInstance( + NodeObject::keyBytes, section, scheduler, ctx, j_); + } - auto const preexist {exists(dir_)}; - auto fail = [this, preexist](std::string const& msg) + using namespace boost::filesystem; + auto preexist {false}; + auto fail = [this, &preexist](std::string const& msg) { pCache_.reset(); nCache_.reset(); backend_.reset(); lgrSQLiteDB_.reset(); txSQLiteDB_.reset(); - storedSeqs_.clear(); - lastStored_.reset(); + acquireInfo_.reset(); if (!preexist) - removeAll(dir_, j_); + remove_all(dir_); if (!msg.empty()) { - JLOG(j_.error()) << - "shard " << index_ << " " << msg; + JLOG(j_.fatal()) << "shard " << index_ << " " << msg; } return false; }; + auto createAcquireInfo = [this, &config]() + { + acquireInfo_ = std::make_unique(); + + DatabaseCon::Setup setup; + setup.startUp = config.START_UP; + setup.standAlone = config.standalone(); + setup.dataDir = dir_; + + acquireInfo_->SQLiteDB = std::make_unique( + setup, + AcquireShardDBName, + AcquireShardDBPragma, + AcquireShardDBInit); + acquireInfo_->SQLiteDB->setupCheckpointing( + &app_.getJobQueue(), + app_.logs()); + }; + try { - // Open/Create the NuDB key/value store for node objects + // Open or create the NuDB key/value store + preexist = exists(dir_); backend_->open(!preexist); - if (!backend_->backed()) - return true; - if (!preexist) { - // New shard, create a control file - if (!saveControl(lock)) - return fail({}); + // A new shard + createAcquireInfo(); + acquireInfo_->SQLiteDB->getSession() << + "INSERT INTO Shard (ShardIndex) " + "VALUES (:shardIndex);" + , soci::use(index_); } - else if (is_regular_file(control_)) + else if (exists(dir_ / AcquireShardDBName)) { - // Incomplete shard, inspect control file - std::ifstream ifs(control_.string()); - if (!ifs.is_open()) - return fail("failed to open control file"); - - boost::archive::text_iarchive ar(ifs); - ar & storedSeqs_; - if (!storedSeqs_.empty()) + // An incomplete shard, being acquired + createAcquireInfo(); + + auto& session {acquireInfo_->SQLiteDB->getSession()}; + boost::optional index; + soci::blob sociBlob(session); + soci::indicator blobPresent; + + session << + "SELECT ShardIndex, StoredLedgerSeqs " + "FROM Shard " + "WHERE ShardIndex = :index;" + , soci::into(index) + , soci::into(sociBlob, blobPresent) + , soci::use(index_); + + if (!index || index != index_) + return fail("invalid acquire SQLite database"); + + if (blobPresent == soci::i_ok) { - if (boost::icl::first(storedSeqs_) < firstSeq_ || - boost::icl::last(storedSeqs_) > lastSeq_) + std::string s; + auto& storedSeqs {acquireInfo_->storedSeqs}; + if (convert(sociBlob, s); !from_string(storedSeqs, s)) + return fail("invalid StoredLedgerSeqs"); + + if (boost::icl::first(storedSeqs) < firstSeq_ || + boost::icl::last(storedSeqs) > lastSeq_) { - return fail("has an invalid control file"); + return fail("invalid StoredLedgerSeqs"); } - if (boost::icl::length(storedSeqs_) >= maxLedgers_) + if (boost::icl::length(storedSeqs) == maxLedgers_) { - JLOG(j_.warn()) << - "shard " << index_ << - " has a control file for complete shard"; - setComplete(lock); + // All ledgers have been acquired, shard is complete + acquireInfo_.reset(); + backendComplete_ = true; } } } else - setComplete(lock); - - if (!complete_) { - setCache(lock); - if (!initSQLite(lock) ||!setFileStats(lock)) - return fail({}); + // A finalized shard or has all ledgers stored in the backend + std::shared_ptr nObj; + if (backend_->fetch(finalKey.data(), &nObj) != Status::ok) + { + legacy_ = true; + return fail("incompatible, missing backend final key"); + } + + // Check final key's value + SerialIter sIt(nObj->getData().data(), nObj->getData().size()); + if (sIt.get32() != version) + return fail("invalid version"); + + if (sIt.get32() != firstSeq_ || sIt.get32() != lastSeq_) + return fail("out of range ledger sequences"); + + if (sIt.get256().isZero()) + return fail("invalid last ledger hash"); + + if (exists(dir_ / LgrDBName) && exists(dir_ / TxDBName)) + final_ = true; + + backendComplete_ = true; } } catch (std::exception const& e) @@ -155,66 +206,104 @@ Shard::open(Scheduler& scheduler, nudb::context& ctx) e.what() + " in function " + __func__); } + setBackendCache(lock); + if (!initSQLite(lock)) + return fail({}); + + setFileStats(lock); return true; } +boost::optional +Shard::prepare() +{ + std::lock_guard lock(mutex_); + assert(backend_); + + if (backendComplete_) + { + JLOG(j_.warn()) << + "shard " << index_ << + " prepare called when shard is complete"; + return {}; + } + + assert(acquireInfo_); + auto const& storedSeqs {acquireInfo_->storedSeqs}; + if (storedSeqs.empty()) + return lastSeq_; + return prevMissing(storedSeqs, 1 + lastSeq_, firstSeq_); +} + bool -Shard::setStored(std::shared_ptr const& ledger) +Shard::store(std::shared_ptr const& ledger) { + auto const seq {ledger->info().seq}; + if (seq < firstSeq_ || seq > lastSeq_) + { + JLOG(j_.error()) << + "shard " << index_ << + " invalid ledger sequence " << seq; + return false; + } + std::lock_guard lock(mutex_); - assert(backend_ && !complete_); + assert(backend_); - if (boost::icl::contains(storedSeqs_, ledger->info().seq)) + if (backendComplete_) { JLOG(j_.debug()) << "shard " << index_ << - " has ledger sequence " << ledger->info().seq << " already stored"; - return false; + " ledger sequence " << seq << " already stored"; + return true; + } + + assert(acquireInfo_); + auto& storedSeqs {acquireInfo_->storedSeqs}; + if (boost::icl::contains(storedSeqs, seq)) + { + JLOG(j_.debug()) << + "shard " << index_ << + " ledger sequence " << seq << " already stored"; + return true; } + // storeSQLite looks at storedSeqs so insert before the call + storedSeqs.insert(seq); - if (!setSQLiteStored(ledger, lock)) + if (!storeSQLite(ledger, lock)) return false; - // Check if the shard is complete - if (boost::icl::length(storedSeqs_) >= maxLedgers_ - 1) - setComplete(lock); - else + if (boost::icl::length(storedSeqs) >= maxLedgers_) { - storedSeqs_.insert(ledger->info().seq); - if (backend_->backed() && !saveControl(lock)) + if (!initSQLite(lock)) return false; + + acquireInfo_.reset(); + backendComplete_ = true; + setBackendCache(lock); } JLOG(j_.debug()) << "shard " << index_ << - " stored ledger sequence " << ledger->info().seq << - (complete_ ? " and is complete" : ""); + " stored ledger sequence " << seq << + (backendComplete_ ? " . All ledgers stored" : ""); - lastStored_ = ledger; + setFileStats(lock); return true; } -boost::optional -Shard::prepare() -{ - std::lock_guard lock(mutex_); - assert(backend_ && !complete_); - - if (storedSeqs_.empty()) - return lastSeq_; - return prevMissing(storedSeqs_, 1 + lastSeq_, firstSeq_); -} - bool -Shard::contains(std::uint32_t seq) const +Shard::containsLedger(std::uint32_t seq) const { if (seq < firstSeq_ || seq > lastSeq_) return false; std::lock_guard lock(mutex_); - assert(backend_); + if (backendComplete_) + return true; - return complete_ || boost::icl::contains(storedSeqs_, seq); + assert(acquireInfo_); + return boost::icl::contains(acquireInfo_->storedSeqs, seq); } void @@ -227,7 +316,19 @@ Shard::sweep() nCache_->sweep(); } -std::shared_ptr const& +std::tuple< + std::shared_ptr, + std::shared_ptr, + std::shared_ptr> +Shard::getBackendAll() const +{ + std::lock_guard lock(mutex_); + assert(backend_); + + return {backend_, pCache_, nCache_}; +} + +std::shared_ptr Shard::getBackend() const { std::lock_guard lock(mutex_); @@ -237,12 +338,10 @@ Shard::getBackend() const } bool -Shard::complete() const +Shard::isBackendComplete() const { std::lock_guard lock(mutex_); - assert(backend_); - - return complete_; + return backendComplete_; } std::shared_ptr @@ -263,94 +362,160 @@ Shard::nCache() const return nCache_; } -std::uint64_t -Shard::fileSize() const +std::pair +Shard::fileInfo() const { std::lock_guard lock(mutex_); - assert(backend_); - - return fileSz_; + return {fileSz_, fdRequired_}; } -std::uint32_t -Shard::fdRequired() const +bool +Shard::isFinal() const { std::lock_guard lock(mutex_); - assert(backend_); - - return fdRequired_; + return final_; } -std::shared_ptr -Shard::lastStored() const +bool +Shard::isLegacy() const { std::lock_guard lock(mutex_); - assert(backend_); - - return lastStored_; + return legacy_; } bool -Shard::validate() const +Shard::finalize(const bool writeSQLite) { - uint256 hash; + assert(backend_); + + if (stop_) + return false; + + uint256 hash {0}; std::uint32_t seq {0}; auto fail = [j = j_, index = index_, &hash, &seq](std::string const& msg) { - JLOG(j.error()) << + JLOG(j.fatal()) << "shard " << index << ". " << msg << (hash.isZero() ? "" : ". Ledger hash " + to_string(hash)) << (seq == 0 ? "" : ". Ledger sequence " + std::to_string(seq)); return false; }; - std::shared_ptr ledger; - // Find the hash of the last ledger in this shard + try { - std::tie(ledger, seq, hash) = loadLedgerHelper( - "WHERE LedgerSeq >= " + std::to_string(lastSeq_) + - " order by LedgerSeq desc limit 1", app_, false); - if (!ledger) - return fail("Unable to validate due to lacking lookup data"); + std::unique_lock lock(mutex_); + if (!backendComplete_) + return fail("incomplete"); + + /* + TODO MP + A lock is required when calling the NuDB verify function. Because + this can be a time consuming process, the server may desync. + Until this function is modified to work on an open database, we + are unable to use it from rippled. + + // Verify backend integrity + backend_->verify(); + */ + + // Check if a final key has been stored + lock.unlock(); + if (std::shared_ptr nObj; + backend_->fetch(finalKey.data(), &nObj) == Status::ok) + { + // Check final key's value + SerialIter sIt(nObj->getData().data(), nObj->getData().size()); + if (sIt.get32() != version) + return fail("invalid version"); - if (seq != lastSeq_) + if (sIt.get32() != firstSeq_ || sIt.get32() != lastSeq_) + return fail("out of range ledger sequences"); + + if (hash = sIt.get256(); hash.isZero()) + return fail("invalid last ledger hash"); + } + else { - boost::optional h; + // In the absence of a final key, an acquire SQLite database + // must be present in order to validate the shard + lock.lock(); + if (!acquireInfo_) + return fail("missing acquire SQLite database"); + + auto& session {acquireInfo_->SQLiteDB->getSession()}; + boost::optional index; + boost::optional sHash; + soci::blob sociBlob(session); + soci::indicator blobPresent; + session << + "SELECT ShardIndex, LastLedgerHash, StoredLedgerSeqs " + "FROM Shard " + "WHERE ShardIndex = :index;" + , soci::into(index) + , soci::into(sHash) + , soci::into(sociBlob, blobPresent) + , soci::use(index_); - ledger->setImmutable(app_.config()); - try - { - h = hashOfSeq(*ledger, lastSeq_, j_); - } - catch (std::exception const& e) + lock.unlock(); + if (!index || index != index_) + return fail("missing or invalid ShardIndex"); + + if (!sHash) + return fail("missing LastLedgerHash"); + + if (hash.SetHexExact(*sHash); hash.isZero()) + return fail("invalid LastLedgerHash"); + + if (blobPresent != soci::i_ok) + return fail("missing StoredLedgerSeqs"); + + std::string s; + convert(sociBlob, s); + + lock.lock(); + + auto& storedSeqs {acquireInfo_->storedSeqs}; + if (!from_string(storedSeqs, s) || + boost::icl::first(storedSeqs) != firstSeq_ || + boost::icl::last(storedSeqs) != lastSeq_ || + storedSeqs.size() != maxLedgers_) { - return fail(std::string("exception ") + - e.what() + " in function " + __func__); + return fail("invalid StoredLedgerSeqs"); } - - if (!h) - return fail("Missing hash for last ledger sequence"); - hash = *h; - seq = lastSeq_; } } + catch (std::exception const& e) + { + return fail(std::string("exception ") + + e.what() + " in function " + __func__); + } - // Validate every ledger stored in this shard + // Validate every ledger stored in the backend + std::shared_ptr ledger; std::shared_ptr next; + auto const lastLedgerHash {hash}; + + // Start with the last ledger in the shard and walk backwards from + // child to parent until we reach the first ledger + seq = lastSeq_; while (seq >= firstSeq_) { + if (stop_) + return false; + auto nObj = valFetch(hash); if (!nObj) - return fail("Invalid ledger"); + return fail("invalid ledger"); ledger = std::make_shared( InboundLedger::deserializeHeader(makeSlice(nObj->getData()), true), app_.config(), *app_.shardFamily()); if (ledger->info().seq != seq) - return fail("Invalid ledger header sequence"); + return fail("invalid ledger sequence"); if (ledger->info().hash != hash) - return fail("Invalid ledger header hash"); + return fail("invalid ledger hash"); ledger->stateMap().setLedgerSeq(seq); ledger->txMap().setLedgerSeq(seq); @@ -358,96 +523,138 @@ Shard::validate() const if (!ledger->stateMap().fetchRoot( SHAMapHash {ledger->info().accountHash}, nullptr)) { - return fail("Missing root STATE node"); + return fail("missing root STATE node"); } if (ledger->info().txHash.isNonZero() && !ledger->txMap().fetchRoot( SHAMapHash {ledger->info().txHash}, nullptr)) { - return fail("Missing root TXN node"); + return fail("missing root TXN node"); } if (!valLedger(ledger, next)) - return false; + return fail("failed to validate ledger"); + + if (writeSQLite) + { + std::lock_guard lock(mutex_); + if (!storeSQLite(ledger, lock)) + return fail("failed storing to SQLite databases"); + } hash = ledger->info().parentHash; --seq; next = ledger; } - { - std::lock_guard lock(mutex_); - pCache_->reset(); - nCache_->reset(); - } - JLOG(j_.debug()) << "shard " << index_ << " is valid"; - return true; -} -bool -Shard::setComplete(std::lock_guard const& lock) -{ - // Remove the control file if one exists + /* + TODO MP + SQLite VACUUM blocks all database access while processing. + Depending on the file size, that can take a while. Until we find + a non-blocking way of doing this, we cannot enable vacuum as + it can desync a server. + try { - using namespace boost::filesystem; - if (is_regular_file(control_)) - remove_all(control_); + // VACUUM the SQLite databases + auto const tmpDir {dir_ / "tmp_vacuum"}; + create_directory(tmpDir); + auto vacuum = [&tmpDir](std::unique_ptr& sqliteDB) + { + auto& session {sqliteDB->getSession()}; + session << "PRAGMA synchronous=OFF;"; + session << "PRAGMA journal_mode=OFF;"; + session << "PRAGMA temp_store_directory='" << + tmpDir.string() << "';"; + session << "VACUUM;"; + }; + vacuum(lgrSQLiteDB_); + vacuum(txSQLiteDB_); + remove_all(tmpDir); } catch (std::exception const& e) { - JLOG(j_.error()) << - "shard " << index_ << - " exception " << e.what() << - " in function " << __func__; - return false; + return fail(std::string("exception ") + + e.what() + " in function " + __func__); } + */ + + // Store final key's value, may already be stored + Serializer s; + s.add32(version); + s.add32(firstSeq_); + s.add32(lastSeq_); + s.add256(lastLedgerHash); + auto nObj {NodeObject::createObject( + hotUNKNOWN, + std::move(s.modData()), + finalKey)}; + try + { + backend_->store(nObj); + + std::lock_guard lock(mutex_); + final_ = true; + + // Remove the acquire SQLite database if present + if (acquireInfo_) + acquireInfo_.reset(); + remove_all(dir_ / AcquireShardDBName); - storedSeqs_.clear(); - complete_ = true; + if (!initSQLite(lock)) + return fail("failed to initialize SQLite databases"); - setCache(lock); - return initSQLite(lock) && setFileStats(lock); + setFileStats(lock); + } + catch (std::exception const& e) + { + return fail(std::string("exception ") + + e.what() + " in function " + __func__); + } + + return true; } void -Shard::setCache(std::lock_guard const&) +Shard::setBackendCache(std::lock_guard const&) { - // complete shards use the smallest cache and + // Complete shards use the smallest cache and // fastest expiration to reduce memory consumption. - // The incomplete shard is set according to configuration. + // An incomplete shard is set according to configuration. + + Config const& config {app_.config()}; if (!pCache_) { auto const name {"shard " + std::to_string(index_)}; - auto const sz = app_.config().getValueFor(SizedItem::nodeCacheSize, - complete_ ? boost::optional(0) : boost::none); - auto const age = std::chrono::seconds{ - app_.config().getValueFor(SizedItem::nodeCacheAge, - complete_ ? boost::optional(0) : boost::none)}; + auto const sz {config.getValueFor( + SizedItem::nodeCacheSize, + backendComplete_ ? boost::optional(0) : boost::none)}; + auto const age {std::chrono::seconds{config.getValueFor( + SizedItem::nodeCacheAge, + backendComplete_ ? boost::optional(0) : boost::none)}}; pCache_ = std::make_shared(name, sz, age, stopwatch(), j_); nCache_ = std::make_shared(name, stopwatch(), sz, age); } else { - auto const sz = app_.config().getValueFor( - SizedItem::nodeCacheSize, 0); + auto const sz {config.getValueFor(SizedItem::nodeCacheSize, 0)}; pCache_->setTargetSize(sz); nCache_->setTargetSize(sz); - auto const age = std::chrono::seconds{ - app_.config().getValueFor( - SizedItem::nodeCacheAge, 0)}; + auto const age {std::chrono::seconds{ + config.getValueFor(SizedItem::nodeCacheAge, 0)}}; pCache_->setTargetAge(age); nCache_->setTargetAge(age); } } bool -Shard::initSQLite(std::lock_guard const&) +Shard::initSQLite(std::lock_guard const&) { Config const& config {app_.config()}; DatabaseCon::Setup setup; @@ -457,61 +664,40 @@ Shard::initSQLite(std::lock_guard const&) try { - if (complete_) - { - // Remove WAL files if they exist - using namespace boost::filesystem; - for (auto const& d : directory_iterator(dir_)) - { - if (is_regular_file(d) && - boost::iends_with(extension(d), "-wal")) - { - // Closing the session forces a checkpoint - if (!lgrSQLiteDB_) - { - lgrSQLiteDB_ = std::make_unique ( - setup, - LgrDBName, - LgrDBPragma, - LgrDBInit); - } - lgrSQLiteDB_->getSession().close(); + if (lgrSQLiteDB_) + lgrSQLiteDB_.reset(); - if (!txSQLiteDB_) - { - txSQLiteDB_ = std::make_unique ( - setup, - TxDBName, - TxDBPragma, - TxDBInit); - } - txSQLiteDB_->getSession().close(); - break; - } - } + if (txSQLiteDB_) + txSQLiteDB_.reset(); - lgrSQLiteDB_ = std::make_unique ( + if (backendComplete_) + { + lgrSQLiteDB_ = std::make_unique( setup, LgrDBName, CompleteShardDBPragma, LgrDBInit); lgrSQLiteDB_->getSession() << boost::str(boost::format("PRAGMA cache_size=-%d;") % - kilobytes(config.getValueFor(SizedItem::lgrDBCache, boost::none))); + kilobytes(config.getValueFor( + SizedItem::lgrDBCache, + boost::none))); - txSQLiteDB_ = std::make_unique ( + txSQLiteDB_ = std::make_unique( setup, TxDBName, CompleteShardDBPragma, TxDBInit); txSQLiteDB_->getSession() << boost::str(boost::format("PRAGMA cache_size=-%d;") % - kilobytes(config.getValueFor(SizedItem::txnDBCache, boost::none))); + kilobytes(config.getValueFor( + SizedItem::txnDBCache, + boost::none))); } else { // The incomplete shard uses a Write Ahead Log for performance - lgrSQLiteDB_ = std::make_unique ( + lgrSQLiteDB_ = std::make_unique( setup, LgrDBName, LgrDBPragma, @@ -521,7 +707,7 @@ Shard::initSQLite(std::lock_guard const&) kilobytes(config.getValueFor(SizedItem::lgrDBCache))); lgrSQLiteDB_->setupCheckpointing(&app_.getJobQueue(), app_.logs()); - txSQLiteDB_ = std::make_unique ( + txSQLiteDB_ = std::make_unique( setup, TxDBName, TxDBPragma, @@ -534,7 +720,7 @@ Shard::initSQLite(std::lock_guard const&) } catch (std::exception const& e) { - JLOG(j_.error()) << + JLOG(j_.fatal()) << "shard " << index_ << " exception " << e.what() << " in function " << __func__; @@ -544,25 +730,29 @@ Shard::initSQLite(std::lock_guard const&) } bool -Shard::setSQLiteStored( +Shard::storeSQLite( std::shared_ptr const& ledger, - std::lock_guard const&) + std::lock_guard const&) { + if (stop_) + return false; + auto const seq {ledger->info().seq}; - assert(backend_ && !complete_); - assert(!boost::icl::contains(storedSeqs_, seq)); try { + // Update the transactions database { auto& session {txSQLiteDB_->getSession()}; soci::transaction tr(session); session << - "DELETE FROM Transactions WHERE LedgerSeq = :seq;" + "DELETE FROM Transactions " + "WHERE LedgerSeq = :seq;" , soci::use(seq); session << - "DELETE FROM AccountTransactions WHERE LedgerSeq = :seq;" + "DELETE FROM AccountTransactions " + "WHERE LedgerSeq = :seq;" , soci::use(seq); if (ledger->info().txHash.isNonZero()) @@ -579,24 +769,29 @@ Shard::setSQLiteStored( for (auto const& item : ledger->txs) { + if (stop_) + return false; + auto const txID {item.first->getTransactionID()}; auto const sTxID {to_string(txID)}; auto const txMeta {std::make_shared( txID, ledger->seq(), *item.second)}; session << - "DELETE FROM AccountTransactions WHERE TransID = :txID;" + "DELETE FROM AccountTransactions " + "WHERE TransID = :txID;" , soci::use(sTxID); auto const& accounts = txMeta->getAffectedAccounts(j_); if (!accounts.empty()) { - auto const s(boost::str(boost::format( + auto const sTxnSeq {std::to_string(txMeta->getIndex())}; + auto const s {boost::str(boost::format( "('%s','%s',%s,%s)") % sTxID % "%s" % sSeq - % std::to_string(txMeta->getIndex()))); + % sTxnSeq)}; std::string sql; sql.reserve((accounts.size() + 1) * 128); sql = "INSERT INTO AccountTransactions " @@ -638,37 +833,81 @@ Shard::setSQLiteStored( tr.commit (); } - auto& session {lgrSQLiteDB_->getSession()}; - soci::transaction tr(session); - - session << - "DELETE FROM Ledgers WHERE LedgerSeq = :seq;" - , soci::use(seq); - session << - "INSERT OR REPLACE INTO Ledgers (" - "LedgerHash, LedgerSeq, PrevHash, TotalCoins, ClosingTime," - "PrevClosingTime, CloseTimeRes, CloseFlags, AccountSetHash," - "TransSetHash)" - "VALUES (" - ":ledgerHash, :ledgerSeq, :prevHash, :totalCoins, :closingTime," - ":prevClosingTime, :closeTimeRes, :closeFlags, :accountSetHash," - ":transSetHash);", - soci::use(to_string(ledger->info().hash)), - soci::use(seq), - soci::use(to_string(ledger->info().parentHash)), - soci::use(to_string(ledger->info().drops)), - soci::use(ledger->info().closeTime.time_since_epoch().count()), - soci::use(ledger->info().parentCloseTime.time_since_epoch().count()), - soci::use(ledger->info().closeTimeResolution.count()), - soci::use(ledger->info().closeFlags), - soci::use(to_string(ledger->info().accountHash)), - soci::use(to_string(ledger->info().txHash)); - - tr.commit(); + auto const sHash {to_string(ledger->info().hash)}; + + // Update the ledger database + { + auto& session {lgrSQLiteDB_->getSession()}; + soci::transaction tr(session); + + auto const sParentHash {to_string(ledger->info().parentHash)}; + auto const sDrops {to_string(ledger->info().drops)}; + auto const sAccountHash {to_string(ledger->info().accountHash)}; + auto const sTxHash {to_string(ledger->info().txHash)}; + + session << + "DELETE FROM Ledgers " + "WHERE LedgerSeq = :seq;" + , soci::use(seq); + session << + "INSERT OR REPLACE INTO Ledgers (" + "LedgerHash, LedgerSeq, PrevHash, TotalCoins, ClosingTime," + "PrevClosingTime, CloseTimeRes, CloseFlags, AccountSetHash," + "TransSetHash)" + "VALUES (" + ":ledgerHash, :ledgerSeq, :prevHash, :totalCoins," + ":closingTime, :prevClosingTime, :closeTimeRes," + ":closeFlags, :accountSetHash, :transSetHash);", + soci::use(sHash), + soci::use(seq), + soci::use(sParentHash), + soci::use(sDrops), + soci::use(ledger->info().closeTime.time_since_epoch().count()), + soci::use( + ledger->info().parentCloseTime.time_since_epoch().count()), + soci::use(ledger->info().closeTimeResolution.count()), + soci::use(ledger->info().closeFlags), + soci::use(sAccountHash), + soci::use(sTxHash); + + tr.commit(); + } + + // Update the acquire database if present + if (acquireInfo_) + { + auto& session {acquireInfo_->SQLiteDB->getSession()}; + soci::blob sociBlob(session); + + if (!acquireInfo_->storedSeqs.empty()) + convert(to_string(acquireInfo_->storedSeqs), sociBlob); + + if (ledger->info().seq == lastSeq_) + { + // Store shard's last ledger hash + session << + "UPDATE Shard " + "SET LastLedgerHash = :lastLedgerHash," + "StoredLedgerSeqs = :storedLedgerSeqs " + "WHERE ShardIndex = :shardIndex;" + , soci::use(sHash) + , soci::use(sociBlob) + , soci::use(index_); + } + else + { + session << + "UPDATE Shard " + "SET StoredLedgerSeqs = :storedLedgerSeqs " + "WHERE ShardIndex = :shardIndex;" + , soci::use(sociBlob) + , soci::use(index_); + } + } } catch (std::exception const& e) { - JLOG(j_.error()) << + JLOG(j_.fatal()) << "shard " << index_ << " exception " << e.what() << " in function " << __func__; @@ -677,51 +916,30 @@ Shard::setSQLiteStored( return true; } -bool -Shard::setFileStats(std::lock_guard const&) +void +Shard::setFileStats(std::lock_guard const&) { fileSz_ = 0; fdRequired_ = 0; - if (backend_->backed()) + try { - try + using namespace boost::filesystem; + for (auto const& d : directory_iterator(dir_)) { - using namespace boost::filesystem; - for (auto const& d : directory_iterator(dir_)) + if (is_regular_file(d)) { - if (is_regular_file(d)) - { - fileSz_ += file_size(d); - ++fdRequired_; - } + fileSz_ += file_size(d); + ++fdRequired_; } } - catch (std::exception const& e) - { - JLOG(j_.error()) << - "shard " << index_ << - " exception " << e.what() << - " in function " << __func__; - return false; - } } - return true; -} - -bool -Shard::saveControl(std::lock_guard const&) -{ - std::ofstream ofs {control_.string(), std::ios::trunc}; - if (!ofs.is_open()) + catch (std::exception const& e) { - JLOG(j_.fatal()) << - "shard " << index_ << " is unable to save control file"; - return false; + JLOG(j_.error()) << + "shard " << index_ << + " exception " << e.what() << + " in function " << __func__; } - - boost::archive::text_oarchive ar(ofs); - ar & storedSeqs_; - return true; } bool @@ -731,25 +949,27 @@ Shard::valLedger( { auto fail = [j = j_, index = index_, &ledger](std::string const& msg) { - JLOG(j.error()) << + JLOG(j.fatal()) << "shard " << index << ". " << msg << (ledger->info().hash.isZero() ? - "" : ". Ledger header hash " + + "" : ". Ledger hash " + to_string(ledger->info().hash)) << (ledger->info().seq == 0 ? - "" : ". Ledger header sequence " + + "" : ". Ledger sequence " + std::to_string(ledger->info().seq)); return false; }; if (ledger->info().hash.isZero()) - return fail("Invalid ledger header hash"); + return fail("Invalid ledger hash"); if (ledger->info().accountHash.isZero()) - return fail("Invalid ledger header account hash"); + return fail("Invalid ledger account hash"); bool error {false}; auto visit = [this, &error](SHAMapAbstractNode& node) { + if (stop_) + return false; if (!valFetch(node.getNodeHash().as_uint256())) error = true; return !error; @@ -773,6 +993,8 @@ Shard::valLedger( return fail(std::string("exception ") + e.what() + " in function " + __func__); } + if (stop_) + return false; if (error) return fail("Invalid state map"); } @@ -792,11 +1014,14 @@ Shard::valLedger( return fail(std::string("exception ") + e.what() + " in function " + __func__); } + if (stop_) + return false; if (error) return fail("Invalid transaction map"); } + return true; -}; +} std::shared_ptr Shard::valFetch(uint256 const& hash) const @@ -804,25 +1029,22 @@ Shard::valFetch(uint256 const& hash) const std::shared_ptr nObj; auto fail = [j = j_, index = index_, &hash, &nObj](std::string const& msg) { - JLOG(j.error()) << + JLOG(j.fatal()) << "shard " << index << ". " << msg << ". Node object hash " << to_string(hash); nObj.reset(); return nObj; }; - Status status; try { - { - std::lock_guard lock(mutex_); - status = backend_->fetch(hash.begin(), &nObj); - } - - switch (status) + switch (backend_->fetch(hash.data(), &nObj)) { case ok: - break; + // This verifies that the hash of node object matches the payload + if (nObj->getHash() != sha512Half(makeSlice(nObj->getData()))) + return fail("Node object hash does not match payload"); + return nObj; case notFound: return fail("Missing node object"); case dataCorrupt: @@ -836,7 +1058,6 @@ Shard::valFetch(uint256 const& hash) const return fail(std::string("exception ") + e.what() + " in function " + __func__); } - return nObj; } } // NodeStore diff --git a/src/ripple/nodestore/impl/Shard.h b/src/ripple/nodestore/impl/Shard.h index 687de6c20c8..a75362ba05e 100644 --- a/src/ripple/nodestore/impl/Shard.h +++ b/src/ripple/nodestore/impl/Shard.h @@ -30,29 +30,12 @@ #include #include +#include +#include + namespace ripple { namespace NodeStore { -// Removes a path in its entirety -inline static -bool -removeAll( - boost::filesystem::path const& path, - beast::Journal const& j) -{ - try - { - boost::filesystem::remove_all(path); - } - catch (std::exception const& e) - { - JLOG(j.error()) << - "exception: " << e.what(); - return false; - } - return true; -} - using PCache = TaggedCache; using NCache = KeyCache; class DatabaseShard; @@ -65,7 +48,7 @@ class DatabaseShard; Public functions can be called concurrently from any thread. */ -class Shard +class Shard final { public: Shard( @@ -77,29 +60,37 @@ class Shard bool open(Scheduler& scheduler, nudb::context& ctx); - bool - setStored(std::shared_ptr const& ledger); - boost::optional prepare(); bool - contains(std::uint32_t seq) const; + store(std::shared_ptr const& ledger); + + bool + containsLedger(std::uint32_t seq) const; void sweep(); std::uint32_t - index() const - { - return index_; - } + index() const {return index_;} + + boost::filesystem::path const& + getDir() const {return dir_;} + + std::tuple< + std::shared_ptr, + std::shared_ptr, + std::shared_ptr> + getBackendAll() const; - std::shared_ptr const& + std::shared_ptr getBackend() const; + /** Returns `true` if all shard ledgers have been stored in the backend + */ bool - complete() const; + isBackendComplete() const; std::shared_ptr pCache() const; @@ -107,36 +98,65 @@ class Shard std::shared_ptr nCache() const; - std::uint64_t - fileSize() const; + /** Returns a pair where the first item describes the storage space + utilized and the second item is the number of file descriptors required. + */ + std::pair + fileInfo() const; - std::uint32_t - fdRequired() const; + /** Returns `true` if the shard is complete, validated, and immutable. + */ + bool + isFinal() const; + + /** Returns `true` if the shard is older, without final key data + */ + bool + isLegacy() const; - std::shared_ptr - lastStored() const; + /** Finalize shard by walking its ledgers and verifying each Merkle tree. + @param writeSQLite If true, SQLite entries will be rewritten using + verified backend data. + */ bool - validate() const; + finalize(const bool writeSQLite); + + void + stop() {stop_ = true;} + + // Current shard version + static constexpr std::uint32_t version {2}; + + // The finalKey is a hard coded value of zero. It is used to store + // finalizing shard data to the backend. The data contains a version, + // last ledger's hash, and the first and last ledger sequences. + static uint256 const finalKey; private: - static constexpr auto controlFileName = "control.txt"; + struct AcquireInfo + { + // SQLite database to track information about what has been acquired + std::unique_ptr SQLiteDB; + + // Tracks the sequences of ledgers acquired and stored in the backend + RangeSet storedSeqs; + }; Application& app_; - mutable std::mutex mutex_; + mutable std::recursive_mutex mutex_; // Shard Index std::uint32_t const index_; - // First ledger sequence in this shard + // First ledger sequence in the shard std::uint32_t const firstSeq_; - // Last ledger sequence in this shard + // Last ledger sequence in the shard std::uint32_t const lastSeq_; - // The maximum number of ledgers this shard can store - // The earliest shard may store less ledgers than - // subsequent shards + // The maximum number of ledgers the shard can store + // The earliest shard may store fewer ledgers than subsequent shards std::uint32_t const maxLedgers_; // Database positive cache @@ -148,14 +168,11 @@ class Shard // Path to database files boost::filesystem::path const dir_; - // Path to control file - boost::filesystem::path const control_; - // Storage space utilized by the shard - std::uint64_t fileSz_; + std::uint64_t fileSz_ {0}; // Number of file descriptors required by the shard - std::uint32_t fdRequired_; + std::uint32_t fdRequired_ {0}; // NuDB key/value store for node objects std::shared_ptr backend_; @@ -166,58 +183,54 @@ class Shard // Transaction SQLite database used for indexes std::unique_ptr txSQLiteDB_; + // Tracking information used only when acquiring a shard from the network. + // If the shard is complete, this member will be null. + std::unique_ptr acquireInfo_; + beast::Journal const j_; - // True if shard has its entire ledger range stored - bool complete_ {false}; + // True if backend has stored all ledgers pertaining to the shard + bool backendComplete_ {false}; - // Sequences of ledgers stored with an incomplete shard - RangeSet storedSeqs_; + // Older shard without an acquire database or final key + // Eventually there will be no need for this and should be removed + bool legacy_ {false}; - // Used as an optimization for visitDifferences - std::shared_ptr lastStored_; + // True if the backend has a final key stored + bool final_ {false}; - // Marks shard immutable - // Lock over mutex_ required - bool - setComplete(std::lock_guard const& lock); + // Determines if the shard needs to stop processing for shutdown + std::atomic stop_ {false}; // Set the backend cache // Lock over mutex_ required void - setCache(std::lock_guard const& lock); + setBackendCache(std::lock_guard const& lock); // Open/Create SQLite databases // Lock over mutex_ required bool - initSQLite(std::lock_guard const& lock); + initSQLite(std::lock_guard const& lock); - // Write SQLite entries for a ledger stored in this shard's backend + // Write SQLite entries for this ledger // Lock over mutex_ required bool - setSQLiteStored( + storeSQLite( std::shared_ptr const& ledger, - std::lock_guard const& lock); + std::lock_guard const& lock); // Set storage and file descriptor usage stats // Lock over mutex_ required - bool - setFileStats(std::lock_guard const& lock); - - // Save the control file for an incomplete shard - // Lock over mutex_ required - bool - saveControl(std::lock_guard const& lock); + void + setFileStats(std::lock_guard const& lock); - // Validate this ledger by walking its SHAMaps - // and verifying each merkle tree + // Validate this ledger by walking its SHAMaps and verifying Merkle trees bool valLedger( std::shared_ptr const& ledger, std::shared_ptr const& next) const; - // Fetches from the backend and will log - // errors based on status codes + // Fetches from backend and log errors based on status codes std::shared_ptr valFetch(uint256 const& hash) const; }; diff --git a/src/ripple/nodestore/impl/TaskQueue.cpp b/src/ripple/nodestore/impl/TaskQueue.cpp new file mode 100644 index 00000000000..1ee718679f3 --- /dev/null +++ b/src/ripple/nodestore/impl/TaskQueue.cpp @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple { +namespace NodeStore { + +TaskQueue::TaskQueue(Stoppable& parent) + : Stoppable("TaskQueue", parent) + , workers_(*this, nullptr, "Shard store taskQueue", 1) +{ +} + +void +TaskQueue::onStop() +{ + workers_.pauseAllThreadsAndWait(); + stopped(); +} + +void +TaskQueue::addTask(std::function task) +{ + std::lock_guard lock {mutex_}; + + tasks_.emplace(std::move(task)); + workers_.addTask(); +} + +void +TaskQueue::processTask(int instance) +{ + std::function task; + + { + std::lock_guard lock {mutex_}; + assert(!tasks_.empty()); + + task = std::move(tasks_.front()); + tasks_.pop(); + } + + task(); +} + +} // NodeStore +} // ripple diff --git a/src/ripple/nodestore/impl/TaskQueue.h b/src/ripple/nodestore/impl/TaskQueue.h new file mode 100644 index 00000000000..ab53ea090ed --- /dev/null +++ b/src/ripple/nodestore/impl/TaskQueue.h @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2019 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_NODESTORE_TASKQUEUE_H_INCLUDED +#define RIPPLE_NODESTORE_TASKQUEUE_H_INCLUDED + +#include +#include + +#include +#include + +namespace ripple { +namespace NodeStore { + +class TaskQueue + : public Stoppable + , private Workers::Callback +{ +public: + explicit + TaskQueue(Stoppable& parent); + + void + onStop() override; + + /** Adds a task to the queue + + @param task std::function with signature void() + */ + void + addTask(std::function task); + +private: + std::mutex mutex_; + Workers workers_; + std::queue> tasks_; + + void + processTask(int instance) override; +}; + +} // NodeStore +} // ripple + +#endif diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index 122e3dd197c..7908914e110 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -431,7 +431,7 @@ PeerImp::hasLedger (uint256 const& hash, std::uint32_t seq) const return true; } - return seq >= app_.getNodeStore().earliestSeq() && + return seq >= app_.getNodeStore().earliestLedgerSeq() && hasShard(NodeStore::seqToShardIndex(seq)); } @@ -1259,6 +1259,9 @@ PeerImp::onMessage(std::shared_ptr const& m) // Parse the shard indexes received in the shard info RangeSet shardIndexes; { + if (!from_string(shardIndexes, m->shardindexes())) + return badData("Invalid shard indexes"); + std::uint32_t earliestShard; boost::optional latestShard; { @@ -1267,70 +1270,23 @@ PeerImp::onMessage(std::shared_ptr const& m) if (auto shardStore = app_.getShardStore()) { earliestShard = shardStore->earliestShardIndex(); - if (curLedgerSeq >= shardStore->earliestSeq()) + if (curLedgerSeq >= shardStore->earliestLedgerSeq()) latestShard = shardStore->seqToShardIndex(curLedgerSeq); } else { - auto const earliestSeq {app_.getNodeStore().earliestSeq()}; - earliestShard = NodeStore::seqToShardIndex(earliestSeq); - if (curLedgerSeq >= earliestSeq) + auto const earliestLedgerSeq { + app_.getNodeStore().earliestLedgerSeq()}; + earliestShard = NodeStore::seqToShardIndex(earliestLedgerSeq); + if (curLedgerSeq >= earliestLedgerSeq) latestShard = NodeStore::seqToShardIndex(curLedgerSeq); } } - auto getIndex = [this, &earliestShard, &latestShard] - (std::string const& s) -> boost::optional + if (boost::icl::first(shardIndexes) < earliestShard || + (latestShard && boost::icl::last(shardIndexes) > latestShard)) { - std::uint32_t shardIndex; - if (!beast::lexicalCastChecked(shardIndex, s)) - { - fee_ = Resource::feeBadData; - return boost::none; - } - if (shardIndex < earliestShard || - (latestShard && shardIndex > latestShard)) - { - fee_ = Resource::feeBadData; - JLOG(p_journal_.error()) << - "Invalid shard index " << shardIndex; - return boost::none; - } - return shardIndex; - }; - - std::vector tokens; - boost::split(tokens, m->shardindexes(), - boost::algorithm::is_any_of(",")); - std::vector indexes; - for (auto const& t : tokens) - { - indexes.clear(); - boost::split(indexes, t, boost::algorithm::is_any_of("-")); - switch (indexes.size()) - { - case 1: - { - auto const first {getIndex(indexes.front())}; - if (!first) - return; - shardIndexes.insert(*first); - break; - } - case 2: - { - auto const first {getIndex(indexes.front())}; - if (!first) - return; - auto const second {getIndex(indexes.back())}; - if (!second) - return; - shardIndexes.insert(range(*first, *second)); - break; - } - default: - return badData("Invalid shard indexes"); - } + return badData("Invalid shard indexes"); } } @@ -1340,7 +1296,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (m->endpoint() != "0") { - auto result = + auto result = beast::IP::Endpoint::from_string_checked(m->endpoint()); if (!result) return badData("Invalid incoming endpoint: " + m->endpoint()); @@ -2268,7 +2224,7 @@ PeerImp::onMessage (std::shared_ptr const& m) { if (auto shardStore = app_.getShardStore()) { - if (seq >= shardStore->earliestSeq()) + if (seq >= shardStore->earliestLedgerSeq()) hObj = shardStore->fetch(hash, seq); } } @@ -2714,7 +2670,7 @@ PeerImp::getLedger (std::shared_ptr const& m) if (auto shardStore = app_.getShardStore()) { auto seq = packet.ledgerseq(); - if (seq >= shardStore->earliestSeq()) + if (seq >= shardStore->earliestLedgerSeq()) ledger = shardStore->fetchLedger(ledgerhash, seq); } } diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index e14b15e28de..46608ed4d5a 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -554,7 +554,6 @@ JSS ( url_password ); // in: Subscribe JSS ( url_username ); // in: Subscribe JSS ( urlgravatar ); // JSS ( username ); // in: Subscribe -JSS ( validate ); // in: DownloadShard JSS ( validated ); // out: NetworkOPs, RPCHelpers, AccountTx* // Tx JSS ( validator_list_expires ); // out: NetworkOps, ValidatorList diff --git a/src/ripple/rpc/ShardArchiveHandler.h b/src/ripple/rpc/ShardArchiveHandler.h index ed7bc2a5cea..f1026603a8d 100644 --- a/src/ripple/rpc/ShardArchiveHandler.h +++ b/src/ripple/rpc/ShardArchiveHandler.h @@ -41,8 +41,7 @@ class ShardArchiveHandler ShardArchiveHandler& operator= (ShardArchiveHandler&&) = delete; ShardArchiveHandler& operator= (ShardArchiveHandler const&) = delete; - /** @param validate if shard data should be verified with network. */ - ShardArchiveHandler(Application& app, bool validate); + ShardArchiveHandler(Application& app); ~ShardArchiveHandler(); @@ -80,7 +79,6 @@ class ShardArchiveHandler Application& app_; std::shared_ptr downloader_; boost::filesystem::path const downloadDir_; - bool const validate_; boost::asio::basic_waitable_timer timer_; bool process_; std::map archives_; diff --git a/src/ripple/rpc/handlers/DownloadShard.cpp b/src/ripple/rpc/handlers/DownloadShard.cpp index 6b943f665b5..174867ce1ee 100644 --- a/src/ripple/rpc/handlers/DownloadShard.cpp +++ b/src/ripple/rpc/handlers/DownloadShard.cpp @@ -34,7 +34,6 @@ namespace ripple { /** RPC command that downloads and import shard archives. { shards: [{index: , url: }] - validate: // optional, default is true } example: @@ -124,20 +123,9 @@ doDownloadShard(RPC::JsonContext& context) } } - bool validate {true}; - if (context.params.isMember(jss::validate)) - { - if (!context.params[jss::validate].isBool()) - { - return RPC::expected_field_error( - std::string(jss::validate), "a bool"); - } - validate = context.params[jss::validate].asBool(); - } - // Begin downloading. The handler keeps itself alive while downloading. auto handler { - std::make_shared(context.app, validate)}; + std::make_shared(context.app)}; for (auto& [index, url] : archives) { if (!handler->add(index, std::move(url))) diff --git a/src/ripple/rpc/impl/ShardArchiveHandler.cpp b/src/ripple/rpc/impl/ShardArchiveHandler.cpp index dc20704c0dc..1405b52da1d 100644 --- a/src/ripple/rpc/impl/ShardArchiveHandler.cpp +++ b/src/ripple/rpc/impl/ShardArchiveHandler.cpp @@ -31,11 +31,10 @@ namespace RPC { using namespace boost::filesystem; using namespace std::chrono_literals; -ShardArchiveHandler::ShardArchiveHandler(Application& app, bool validate) +ShardArchiveHandler::ShardArchiveHandler(Application& app) : app_(app) , downloadDir_(get(app_.config().section( ConfigSection::shardDatabase()), "path", "") + "/download") - , validate_(validate) , timer_(app_.getIOService()) , process_(false) , j_(app.journal("ShardArchiveHandler")) @@ -209,7 +208,7 @@ ShardArchiveHandler::complete(path dstPath) { // If validating and not synced then defer and retry auto const mode {ptr->app_.getOPs().getOperatingMode()}; - if (ptr->validate_ && mode != OperatingMode::FULL) + if (mode != OperatingMode::FULL) { std::lock_guard lock(m_); timer_.expires_from_now(static_cast( @@ -265,7 +264,7 @@ ShardArchiveHandler::process(path const& dstPath) } // Import the shard into the shard store - if (!app_.getShardStore()->importShard(shardIndex, shardDir, validate_)) + if (!app_.getShardStore()->importShard(shardIndex, shardDir)) { JLOG(j_.error()) << "Importing shard " << shardIndex; diff --git a/src/ripple/shamap/impl/SHAMapNodeID.cpp b/src/ripple/shamap/impl/SHAMapNodeID.cpp index a284a1d7b8a..9b3526e90df 100644 --- a/src/ripple/shamap/impl/SHAMapNodeID.cpp +++ b/src/ripple/shamap/impl/SHAMapNodeID.cpp @@ -112,24 +112,6 @@ SHAMapNodeID SHAMapNodeID::getChildNodeID (int m) const // Which branch would contain the specified hash int SHAMapNodeID::selectBranch (uint256 const& hash) const { -#if RIPPLE_VERIFY_NODEOBJECT_KEYS - - if (mDepth >= 64) - { - assert (false); - return -1; - } - - if ((hash & Masks(mDepth)) != mNodeID) - { - std::cerr << "selectBranch(" << getString () << std::endl; - std::cerr << " " << hash << " off branch" << std::endl; - assert (false); - return -1; // does not go under this node - } - -#endif - int branch = * (hash.begin () + (mDepth / 2)); if (mDepth & 1) diff --git a/src/test/basics/RangeSet_test.cpp b/src/test/basics/RangeSet_test.cpp index 2318398bfd3..d46fd4467b7 100644 --- a/src/test/basics/RangeSet_test.cpp +++ b/src/test/basics/RangeSet_test.cpp @@ -19,8 +19,6 @@ #include #include -#include -#include namespace ripple { @@ -78,39 +76,73 @@ class RangeSet_test : public beast::unit_test::suite } void - testSerialization() + testFromString() { + testcase("fromString"); - auto works = [](RangeSet const & orig) - { - std::stringstream ss; - boost::archive::binary_oarchive oa(ss); - oa << orig; - - boost::archive::binary_iarchive ia(ss); - RangeSet deser; - ia >> deser; - - return orig == deser; - }; - - RangeSet rs; - - BEAST_EXPECT(works(rs)); - - rs.insert(3); - BEAST_EXPECT(works(rs)); - - rs.insert(range(7u, 10u)); - BEAST_EXPECT(works(rs)); + RangeSet set; + BEAST_EXPECT(!from_string(set, "")); + BEAST_EXPECT(boost::icl::length(set) == 0); + + BEAST_EXPECT(!from_string(set, "#")); + BEAST_EXPECT(boost::icl::length(set) == 0); + + BEAST_EXPECT(!from_string(set, ",")); + BEAST_EXPECT(boost::icl::length(set) == 0); + + BEAST_EXPECT(!from_string(set, ",-")); + BEAST_EXPECT(boost::icl::length(set) == 0); + + BEAST_EXPECT(!from_string(set, "1,,2")); + BEAST_EXPECT(boost::icl::length(set) == 0); + + set.clear(); + BEAST_EXPECT(from_string(set, "1")); + BEAST_EXPECT(boost::icl::length(set) == 1); + BEAST_EXPECT(boost::icl::first(set) == 1); + + set.clear(); + BEAST_EXPECT(from_string(set, "1,1")); + BEAST_EXPECT(boost::icl::length(set) == 1); + BEAST_EXPECT(boost::icl::first(set) == 1); + + set.clear(); + BEAST_EXPECT(from_string(set, "1-1")); + BEAST_EXPECT(boost::icl::length(set) == 1); + BEAST_EXPECT(boost::icl::first(set) == 1); + + set.clear(); + BEAST_EXPECT(from_string(set, "1,4-6")); + BEAST_EXPECT(boost::icl::length(set) == 4); + BEAST_EXPECT(boost::icl::first(set) == 1); + BEAST_EXPECT(!boost::icl::contains(set, 2)); + BEAST_EXPECT(!boost::icl::contains(set, 3)); + BEAST_EXPECT(boost::icl::contains(set, 4)); + BEAST_EXPECT(boost::icl::contains(set, 5)); + BEAST_EXPECT(boost::icl::last(set) == 6); + + set.clear(); + BEAST_EXPECT(from_string(set, "1-2,4-6")); + BEAST_EXPECT(boost::icl::length(set) == 5); + BEAST_EXPECT(boost::icl::first(set) == 1); + BEAST_EXPECT(boost::icl::contains(set, 2)); + BEAST_EXPECT(boost::icl::contains(set, 4)); + BEAST_EXPECT(boost::icl::last(set) == 6); + + set.clear(); + BEAST_EXPECT(from_string(set, "1-2,6")); + BEAST_EXPECT(boost::icl::length(set) == 3); + BEAST_EXPECT(boost::icl::first(set) == 1); + BEAST_EXPECT(boost::icl::contains(set, 2)); + BEAST_EXPECT(boost::icl::last(set) == 6); } void run() override { testPrevMissing(); testToString(); - testSerialization(); + testFromString(); } }; diff --git a/src/test/core/Workers_test.cpp b/src/test/core/Workers_test.cpp index cf3edcc84be..931f6d2206f 100644 --- a/src/test/core/Workers_test.cpp +++ b/src/test/core/Workers_test.cpp @@ -109,7 +109,7 @@ class Workers_test : public beast::unit_test::suite std::unique_ptr perfLog = std::make_unique(); - Workers w(cb, *perfLog, "Test", tc1); + Workers w(cb, perfLog.get(), "Test", tc1); BEAST_EXPECT(w.getNumberOfThreads() == tc1); auto testForThreadCount = [this, &cb, &w] (int const threadCount) diff --git a/src/test/nodestore/Database_test.cpp b/src/test/nodestore/Database_test.cpp index ae38fd63e42..12d37580518 100644 --- a/src/test/nodestore/Database_test.cpp +++ b/src/test/nodestore/Database_test.cpp @@ -165,7 +165,7 @@ class Database_test : public TestBase std::unique_ptr db = Manager::instance().make_Database( "test", scheduler, 2, parent, nodeParams, journal_); - BEAST_EXPECT(db->earliestSeq() == XRP_LEDGER_EARLIEST_SEQ); + BEAST_EXPECT(db->earliestLedgerSeq() == XRP_LEDGER_EARLIEST_SEQ); } // Set an invalid earliest ledger sequence @@ -190,7 +190,7 @@ class Database_test : public TestBase "test", scheduler, 2, parent, nodeParams, journal_); // Verify database uses the earliest ledger sequence setting - BEAST_EXPECT(db->earliestSeq() == 1); + BEAST_EXPECT(db->earliestLedgerSeq() == 1); } diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h index 5343931e72b..e1fd0cfe531 100644 --- a/src/test/nodestore/TestBase.h +++ b/src/test/nodestore/TestBase.h @@ -195,7 +195,7 @@ class TestBase : public beast::unit_test::suite db.store (object->getType (), std::move (data), object->getHash (), - db.earliestSeq()); + db.earliestLedgerSeq()); } } diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index f2ca3758241..bc45dd5c6c1 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -2998,10 +2998,9 @@ static RPCCallTestData const rpcCallTestArray [] = })" }, { - "download_shard: novalidate.", __LINE__, + "download_shard:", __LINE__, { "download_shard", - "novalidate", "20", "url_NotValidated", }, @@ -3016,8 +3015,7 @@ static RPCCallTestData const rpcCallTestArray [] = "index" : 20, "url" : "url_NotValidated" } - ], - "validate" : false + ] } ] })" @@ -3064,10 +3062,9 @@ static RPCCallTestData const rpcCallTestArray [] = })" }, { - "download_shard: novalidate many shards.", __LINE__, + "download_shard: many shards.", __LINE__, { "download_shard", - "novalidate", "2000000", "url_NotValidated0", "2000001", @@ -3106,8 +3103,7 @@ static RPCCallTestData const rpcCallTestArray [] = "index" : 2000004, "url" : "url_NotValidated4" } - ], - "validate" : false + ] } ] })" @@ -3160,8 +3156,7 @@ static RPCCallTestData const rpcCallTestArray [] = "index" : 20, "url" : "url_NotValidated" } - ], - "validate" : false + ] } ] })" From 905a97e0aa02d0c6e1cf198e78c03ff66e282f4f Mon Sep 17 00:00:00 2001 From: Devon White Date: Tue, 3 Dec 2019 13:56:19 -0500 Subject: [PATCH 02/14] Make ShardArchiveHandler downloads more resilient: * Make ShardArchiveHandler a singleton. * Add state database for ShardArchiveHandler. * Use temporary database for SSLHTTPDownloader downloads. * Make ShardArchiveHandler a Stoppable class. * Automatically resume interrupted downloads at server start. --- Builds/CMake/RippledCore.cmake | 2 + src/ripple/app/main/Application.cpp | 48 +++ src/ripple/app/main/DBInit.h | 39 ++ src/ripple/core/DatabaseCon.h | 42 ++- src/ripple/net/DatabaseBody.h | 170 +++++++++ src/ripple/net/DatabaseDownloader.h | 60 +++ src/ripple/net/SSLHTTPDownloader.h | 51 ++- src/ripple/net/ShardDownloader.md | 263 ++++++++++++++ src/ripple/net/images/interrupt_sequence.png | Bin 0 -> 201746 bytes src/ripple/net/images/states.png | Bin 0 -> 119646 bytes src/ripple/net/impl/DatabaseBody.ipp | 312 ++++++++++++++++ src/ripple/net/impl/DatabaseDownloader.cpp | 88 +++++ src/ripple/net/impl/SSLHTTPDownloader.cpp | 316 +++++++++++----- src/ripple/net/uml/interrupt_sequence.pu | 233 ++++++++++++ src/ripple/net/uml/states.pu | 69 ++++ src/ripple/rpc/ShardArchiveHandler.h | 76 +++- src/ripple/rpc/handlers/DownloadShard.cpp | 41 ++- src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/impl/ShardArchiveHandler.cpp | 282 ++++++++++++-- src/test/net/SSLHTTPDownloader_test.cpp | 25 +- src/test/rpc/ShardArchiveHandler_test.cpp | 364 +++++++++++++++++++ 21 files changed, 2291 insertions(+), 192 deletions(-) create mode 100644 src/ripple/net/DatabaseBody.h create mode 100644 src/ripple/net/DatabaseDownloader.h create mode 100644 src/ripple/net/ShardDownloader.md create mode 100644 src/ripple/net/images/interrupt_sequence.png create mode 100644 src/ripple/net/images/states.png create mode 100644 src/ripple/net/impl/DatabaseBody.ipp create mode 100644 src/ripple/net/impl/DatabaseDownloader.cpp create mode 100644 src/ripple/net/uml/interrupt_sequence.pu create mode 100644 src/ripple/net/uml/states.pu create mode 100644 src/test/rpc/ShardArchiveHandler_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index d55ce55801a..ad9925b10ba 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -487,6 +487,7 @@ target_sources (rippled PRIVATE main sources: subdir: net #]===============================] + src/ripple/net/impl/DatabaseDownloader.cpp src/ripple/net/impl/HTTPClient.cpp src/ripple/net/impl/InfoSub.cpp src/ripple/net/impl/RPCCall.cpp @@ -918,6 +919,7 @@ target_sources (rippled PRIVATE src/test/rpc/RPCOverload_test.cpp src/test/rpc/RobustTransaction_test.cpp src/test/rpc/ServerInfo_test.cpp + src/test/rpc/ShardArchiveHandler_test.cpp src/test/rpc/Status_test.cpp src/test/rpc/Submit_test.cpp src/test/rpc/Subscribe_test.cpp diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 93042ce5ebc..d8f223d2e15 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include @@ -1610,6 +1611,53 @@ bool ApplicationImp::setup() } } + if (shardStore_) + { + using namespace boost::filesystem; + + auto stateDb( + RPC::ShardArchiveHandler::getDownloadDirectory(*config_) + / stateDBName); + + try + { + if (exists(stateDb) && + is_regular_file(stateDb) && + !RPC::ShardArchiveHandler::hasInstance()) + { + auto handler = RPC::ShardArchiveHandler::recoverInstance( + *this, + *m_jobQueue); + + assert(handler); + + if (!handler->initFromDB()) + { + JLOG(m_journal.fatal()) + << "Failed to initialize ShardArchiveHandler."; + + return false; + } + + if (!handler->start()) + { + JLOG(m_journal.fatal()) + << "Failed to start ShardArchiveHandler."; + + return false; + } + } + } + catch(std::exception const& e) + { + JLOG(m_journal.fatal()) + << "Exception when starting ShardArchiveHandler from " + "state database: " << e.what(); + + return false; + } + } + return true; } diff --git a/src/ripple/app/main/DBInit.h b/src/ripple/app/main/DBInit.h index af693f708b3..69d9ccf4543 100644 --- a/src/ripple/app/main/DBInit.h +++ b/src/ripple/app/main/DBInit.h @@ -181,6 +181,45 @@ std::array WalletDBInit {{ "END TRANSACTION;" }}; +//////////////////////////////////////////////////////////////////////////////// + +static constexpr auto stateDBName {"state.db"}; + +static constexpr +std::array DownloaderDBPragma +{{ + "PRAGMA synchronous=FULL;", + "PRAGMA journal_mode=DELETE;" +}}; + +static constexpr +std::array ShardArchiveHandlerDBInit +{{ + "BEGIN TRANSACTION;", + + "CREATE TABLE IF NOT EXISTS State ( \ + ShardIndex INTEGER PRIMARY KEY, \ + URL TEXT \ + );", + + "END TRANSACTION;" +}}; + +static constexpr +std::array DatabaseBodyDBInit +{{ + "BEGIN TRANSACTION;", + + "CREATE TABLE IF NOT EXISTS download ( \ + Path TEXT, \ + Data BLOB, \ + Size BIGINT UNSIGNED, \ + Part BIGINT UNSIGNED PRIMARY KEY \ + );", + + "END TRANSACTION;" +}}; + } // ripple #endif diff --git a/src/ripple/core/DatabaseCon.h b/src/ripple/core/DatabaseCon.h index b4dbf6b6d9c..0090df52b0e 100644 --- a/src/ripple/core/DatabaseCon.h +++ b/src/ripple/core/DatabaseCon.h @@ -102,18 +102,17 @@ class DatabaseCon boost::filesystem::path pPath = useTempFiles ? "" : (setup.dataDir / DBName); - open(session_, "sqlite", pPath.string()); + init(pPath, pragma, initSQL); + } - for (auto const& p : pragma) - { - soci::statement st = session_.prepare << p; - st.execute(true); - } - for (auto const& sql : initSQL) - { - soci::statement st = session_.prepare << sql; - st.execute(true); - } + template + DatabaseCon( + boost::filesystem::path const& dataDir, + std::string const& DBName, + std::array const& pragma, + std::array const& initSQL) + { + init((dataDir / DBName), pragma, initSQL); } soci::session& getSession() @@ -129,6 +128,27 @@ class DatabaseCon void setupCheckpointing (JobQueue*, Logs&); private: + + template + void + init(boost::filesystem::path const& pPath, + std::array const& pragma, + std::array const& initSQL) + { + open(session_, "sqlite", pPath.string()); + + for (auto const& p : pragma) + { + soci::statement st = session_.prepare << p; + st.execute(true); + } + for (auto const& sql : initSQL) + { + soci::statement st = session_.prepare << sql; + st.execute(true); + } + } + LockedSociSession::mutex lock_; soci::session session_; diff --git a/src/ripple/net/DatabaseBody.h b/src/ripple/net/DatabaseBody.h new file mode 100644 index 00000000000..c616aeb9b7f --- /dev/null +++ b/src/ripple/net/DatabaseBody.h @@ -0,0 +1,170 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_NET_DATABASEBODY_H +#define RIPPLE_NET_DATABASEBODY_H + +#include +#include +#include +#include +#include + +namespace ripple { + +struct DatabaseBody +{ + // Algorithm for storing buffers when parsing. + class reader; + + // The type of the @ref message::body member. + class value_type; + + /** Returns the size of the body + + @param body The database body to use + */ + static std::uint64_t + size(value_type const& body); +}; + +class DatabaseBody::value_type +{ + // This body container holds a connection to the + // database, and also caches the size when set. + + friend class reader; + friend struct DatabaseBody; + + // The cached file size + std::uint64_t file_size_ = 0; + boost::filesystem::path path_; + std::unique_ptr conn_; + std::string batch_; + std::shared_ptr strand_; + std::mutex m_; + std::condition_variable c_; + uint64_t handler_count_ = 0; + uint64_t part_ = 0; + bool closing_ = false; + +public: + /// Destructor + ~value_type() = default; + + /// Constructor + value_type() = default; + + /// Returns `true` if the file is open + bool + is_open() const + { + return bool{conn_}; + } + + /// Returns the size of the file if open + std::uint64_t + size() const + { + return file_size_; + } + + /// Close the file if open + void + close(); + + /** Open a file at the given path with the specified mode + + @param path The utf-8 encoded path to the file + + @param mode The file mode to use + + @param ec Set to the error, if any occurred + */ + void + open( + boost::filesystem::path path, + Config const& config, + boost::asio::io_service& io_service, + boost::system::error_code& ec); +}; + +/** Algorithm for storing buffers when parsing. + + Objects of this type are created during parsing + to store incoming buffers representing the body. +*/ +class DatabaseBody::reader +{ + value_type& body_; // The body we are writing to + + static const uint32_t FLUSH_SIZE = 50000000; + static const uint8_t MAX_HANDLERS = 3; + static const uint16_t MAX_ROW_SIZE_PAD = 500; + +public: + // Constructor. + // + // This is called after the header is parsed and + // indicates that a non-zero sized body may be present. + // `h` holds the received message headers. + // `b` is an instance of `DatabaseBody`. + // + template + explicit reader( + boost::beast::http::header& h, + value_type& b); + + // Initializer + // + // This is called before the body is parsed and + // gives the reader a chance to do something that might + // need to return an error code. It informs us of + // the payload size (`content_length`) which we can + // optionally use for optimization. + // + void + init(boost::optional const&, boost::system::error_code& ec); + + // This function is called one or more times to store + // buffer sequences corresponding to the incoming body. + // + template + std::size_t + put(ConstBufferSequence const& buffers, boost::system::error_code& ec); + + void + do_put(std::string data); + + // This function is called when writing is complete. + // It is an opportunity to perform any final actions + // which might fail, in order to return an error code. + // Operations that might fail should not be attempted in + // destructors, since an exception thrown from there + // would terminate the program. + // + void + finish(boost::system::error_code& ec); +}; + +} // namespace ripple + +#include + +#endif // RIPPLE_NET_DATABASEBODY_H diff --git a/src/ripple/net/DatabaseDownloader.h b/src/ripple/net/DatabaseDownloader.h new file mode 100644 index 00000000000..ec86242c349 --- /dev/null +++ b/src/ripple/net/DatabaseDownloader.h @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_NET_DATABASEDOWNLOADER_H +#define RIPPLE_NET_DATABASEDOWNLOADER_H + +#include +#include + +namespace ripple { + +class DatabaseDownloader : public SSLHTTPDownloader +{ +public: + DatabaseDownloader( + boost::asio::io_service& io_service, + beast::Journal j, + Config const& config); + +private: + static const uint8_t MAX_PATH_LEN = std::numeric_limits::max(); + + std::shared_ptr + getParser( + boost::filesystem::path dstPath, + std::function complete, + boost::system::error_code& ec) override; + + bool + checkPath(boost::filesystem::path const& dstPath) override; + + void + closeBody(std::shared_ptr p) override; + + uint64_t + size(std::shared_ptr p) override; + + Config const& config_; + boost::asio::io_service& io_service_; +}; + +} // namespace ripple + +#endif // RIPPLE_NET_DATABASEDOWNLOADER_H diff --git a/src/ripple/net/SSLHTTPDownloader.h b/src/ripple/net/SSLHTTPDownloader.h index 893043ce49f..b70383373cb 100644 --- a/src/ripple/net/SSLHTTPDownloader.h +++ b/src/ripple/net/SSLHTTPDownloader.h @@ -50,7 +50,8 @@ class SSLHTTPDownloader SSLHTTPDownloader( boost::asio::io_service& io_service, beast::Journal j, - Config const& config); + Config const& config, + bool isPaused = false); bool download( @@ -61,13 +62,36 @@ class SSLHTTPDownloader boost::filesystem::path const& dstPath, std::function complete); + void + onStop(); + + virtual + ~SSLHTTPDownloader() = default; + +protected: + + using parser = boost::beast::http::basic_parser; + + beast::Journal const j_; + + bool + fail( + boost::filesystem::path dstPath, + std::function const& complete, + boost::system::error_code const& ec, + std::string const& errMsg, + std::shared_ptr parser = nullptr); + private: HTTPClientSSLContext ssl_ctx_; boost::asio::io_service::strand strand_; boost::optional< boost::asio::ssl::stream> stream_; boost::beast::flat_buffer read_buf_; - beast::Journal const j_; + std::atomic isStopped_; + bool sessionActive_; + std::mutex m_; + std::condition_variable c_; void do_session( @@ -79,12 +103,25 @@ class SSLHTTPDownloader std::function complete, boost::asio::yield_context yield); - void - fail( + virtual + std::shared_ptr + getParser( boost::filesystem::path dstPath, - std::function const& complete, - boost::system::error_code const& ec, - std::string const& errMsg); + std::function complete, + boost::system::error_code & ec) = 0; + + virtual + bool + checkPath( + boost::filesystem::path const& dstPath) = 0; + + virtual + void + closeBody(std::shared_ptr p) = 0; + + virtual + uint64_t + size(std::shared_ptr p) = 0; }; } // ripple diff --git a/src/ripple/net/ShardDownloader.md b/src/ripple/net/ShardDownloader.md new file mode 100644 index 00000000000..9ef643a964b --- /dev/null +++ b/src/ripple/net/ShardDownloader.md @@ -0,0 +1,263 @@ +# Shard Downloader Process + +## Overview + +This document describes mechanics of the `SSLHTTPDownloader`, a class that performs the task of downloading shards from remote web servers via +SSL HTTP. The downloader utilizes a strand (`boost::asio::io_service::strand`) to ensure that downloads are never executed concurrently. Hence, if a download is in progress when another download is initiated, the second download will be queued and invoked only when the first download is completed. + +## New Features + +The downloader has been recently (March 2020) been modified to provide some key features: + +- The ability to stop downloads during a graceful shutdown. +- The ability to resume partial downloads after a crash or shutdown. +- *(Deferred) The ability to download from multiple servers to a single file.* + +## Classes + +Much of the shard downloading process concerns the following classes: + +- `SSLHTTPDownloader` + + This is a generic class designed for serially executing downloads via HTTP SSL. + +- `ShardArchiveHandler` + + This class uses the `SSLHTTPDownloader` to fetch shards from remote web servers. Additionally, the archive handler performs sanity checks on the downloaded files and imports the validated files into the local shard store. + + The `ShardArchiveHandler` exposes a simple public interface: + + ```C++ + /** Add an archive to be downloaded and imported. + @param shardIndex the index of the shard to be imported. + @param url the location of the archive. + @return `true` if successfully added. + @note Returns false if called while downloading. + */ + bool + add(std::uint32_t shardIndex, std::pair&& url); + + /** Starts downloading and importing archives. */ + bool + start(); + ``` + + When a client submits a `download_shard` command via the RPC interface, each of the requested files is registered with the handler via the `add` method. After all the files have been registered, the handler's `start` method is invoked, which in turn creates an instance of the `SSLHTTPDownloader` and begins the first download. When the download is completed, the downloader invokes the handler's `complete` method, which will initiate the download of the next file, or simply return if there are no more downloads to process. When `complete` is invoked with no remaining files to be downloaded, the handler and downloader are not destroyed automatically, but persist for the duration of the application. + +Additionally, we leverage a novel class to provide customized parsing for downloaded files: + +- `DatabaseBody` + + This class will define a custom message body type, allowing an `http::response_parser` to write to a SQLite database rather than to a flat file. This class is discussed in further detail in the Recovery section. + +## Execution Concept + +This section describes in greater detail how the key features of the downloader are implemented in C++ using the `boost::asio` framework. + +##### Member Variables: + +The variables shown here are members of the `SSLHTTPDownloader` class and +will be used in the following code examples. + +```c++ +using boost::asio::ssl::stream; +using boost::asio::ip::tcp::socket; + +stream stream_; +std::condition_variable c_; +std::atomic isStopped_; +``` + +### Graceful Shutdowns + +##### Thread 1: + +A graceful shutdown begins when the `onStop()` method of the `ShardArchiveHandler` is invoked: + +```c++ +void +ShardArchiveHandler::onStop() +{ + std::lock_guard lock(m_); + + if (downloader_) + { + downloader_->onStop(); + downloader_.reset(); + } + + stopped(); +} +``` + +Inside of `SSLHTTPDownloader::onStop()`, if a download is currently in progress, the `isStopped_` member variable is set and the thread waits for the download to stop: + +```c++ +void +SSLHTTPDownloader::onStop() +{ + std::unique_lock lock(m_); + + isStopped_ = true; + + if(sessionActive_) + { + // Wait for the handler to exit. + c_.wait(lock, + [this]() + { + return !sessionActive_; + }); + } +} +``` + +##### Thread 2: + +The graceful shutdown is realized when the thread executing the download polls `isStopped_` after this variable has been set to `true`. Polling only occurs while the file is being downloaded, in between calls to `async_read_some()`. The stop takes effect when the socket is closed and the handler function ( `do_session()` ) is exited. + +```c++ +void SSLHTTPDownloader::do_session() +{ + + // (Connection initialization logic) + + . + . + . + + // (In between calls to async_read_some): + if(isStopped_.load()) + { + close(p); + return exit(); + } + + . + . + . + + break; +} +``` + +### Recovery + +Persisting the current state of both the archive handler and the downloader is achieved by leveraging a SQLite database rather than flat files, as the database protects against data corruption that could result from a system crash. + +##### ShardArchiveHandler + +Although `SSLHTTPDownloader` is a generic class that could be used to download a variety of file types, currently it is used exclusively by the `ShardArchiveHandler` to download shards. In order to provide resilience, the `ShardArchiveHandler` will utilize a SQLite database to preserve its current state whenever there are active, paused, or queued downloads. The `shard_db` section in the configuration file allows users to specify the location of the database to use for this purpose. + +###### SQLite Table Format + +| Index | URL | +|:-----:|:-----------------------------------:| +| 1 | https://example.com/1.tar.lz4 | +| 2 | https://example.com/2.tar.lz4 | +| 5 | https://example.com/5.tar.lz4 | + +##### SSLHTTPDownloader + +While the archive handler maintains a list of all partial and queued downloads, the `SSLHTTPDownloader` stores the raw bytes of the file currently being downloaded. The partially downloaded file will be represented as one or more `BLOB` entries in a SQLite database. As the maximum size of a `BLOB` entry is currently limited to roughly 2.1 GB, a 5 GB shard file for instance will occupy three database entries upon completion. + +###### SQLite Table Format + +Since downloads execute serially by design, the entries in this table always correspond to the content of a single file. + +| Bytes | Size | Part | +|:------:|:----------:|:----:| +| 0x... | 2147483647 | 0 | +| 0x... | 2147483647 | 1 | +| 0x... | 705032706 | 2 | + +##### Config File Entry +The `download_path` field of the `shard_db` entry will be used to determine where to store the recovery database. If this field is omitted, the `path` field will be used instead. + +```dosini +# This is the persistent datastore for shards. It is important for the health +# of the ripple network that rippled operators shard as much as practical. +# NuDB requires SSD storage. Helpful information can be found here +# https://ripple.com/build/history-sharding +[shard_db] +type=NuDB +path=/var/lib/rippled/db/shards/nudb +download_path=/var/lib/rippled/db/shards/ +max_size_gb=50 +``` + +##### Resuming Partial Downloads +When resuming downloads after a crash or other interruption, the `SSLHTTPDownloader` will utilize the `range` field of the HTTP header to download only the remainder of the partially downloaded file. + +```C++ +auto downloaded = getPartialFileSize(); +auto total = getTotalFileSize(); + +http::request req {http::verb::head, + target, + version}; + +if (downloaded < total) +{ + // If we already download 1000 bytes to the partial file, + // the range header will look like: + // Range: "bytes=1000-" + req.set(http::field::range, "bytes=" + to_string(downloaded) + "-"); +} +else if(downloaded == total) +{ + // Download is already complete. (Interruption Must + // have occurred after file was downloaded but before + // the state file was updated.) +} +else +{ + // The size of the partially downloaded file exceeds + // the total download size. Error condition. Handle + // appropriately. +} +``` + +##### DatabaseBody + +Previously, the `SSLHTTPDownloader` leveraged an `http::response_parser` instantiated with an `http::file_body`. The `file_body` class declares a nested type, `reader`, which does the task of writing HTTP message payloads (constituting a requested file) to the filesystem. In order for the `http::response_parser` to interface with the database, we implement a custom body type that declares a nested `reader` type which has been outfitted to persist octects received from the remote host to a local SQLite database. The code snippet below illustrates the customization points available to user-defined body types: + +```C++ +/// Defines a Body type +struct body +{ + /// This determines the return type of the `message::body` member function + using value_type = ...; + + /// An optional function, returns the body's payload size (which may be zero) + static + std::uint64_t + size(value_type const& v); + + /// The algorithm used for extracting buffers + class reader; + + /// The algorithm used for inserting buffers + class writer; +} + +``` + +The method invoked to write data to the filesystem (or SQLite database in our case) has the following signature: + +```C++ +std::size_t +body::reader::put(ConstBufferSequence const& buffers, error_code& ec); +``` + +## Sequence Diagram + +This sequence diagram demonstrates a scenario wherein the `ShardArchiveHandler` leverages the state persisted in the database to recover from a crash and resume the scheduled downloads. + +![alt_text](./images/interrupt_sequence.png "Resuming downloads post abort") + +## State Diagram + +This diagram illustrates the various states of the Shard Downloader module. + +![alt_text](./images/states.png "Shard Downloader states") diff --git a/src/ripple/net/images/interrupt_sequence.png b/src/ripple/net/images/interrupt_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..87cc3c8d7966219065d4530663c14c6e9187a758 GIT binary patch literal 201746 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 z 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 literal 0 HcmV?d00001 diff --git a/src/ripple/net/images/states.png b/src/ripple/net/images/states.png new file mode 100644 index 0000000000000000000000000000000000000000..d982955ddf2f3d935d6b21c0b281693dfe1050ea GIT binary patch literal 119646 zcma%jWmuJ4*X~k8R0OtyAl;3iq%^2>cXzkMV$q-oDBWGsA>EDA(%s#)=&m!@j`!W) zcg~OFAHCpt<}>G*BkpmJG5jVgErRw0?+F9~K@$`GAP<2&u!2C4?>)K?eo`Z~jST*x zbr4c@FtGXPY-wcd01+{=HnP)mFft_3cP24)aQMi@#Prcp&)NZMWyxq@WA*GcCjkV4 z1T#}qb@=D!kb7Vmr^Fy7Et_d}EO(rRyN`o06-1V*aT*T7&m5xad)0>MN+V*NU%uR& zqM{2U;!nhVe|Nd1UM@3!{1c9IFuTN6)>rtth1@KKJ_hb|`hw-?98e+sjx< zH>JpL7-7H=FLS$}qHfI-q^}=TEU27rCl9S6yHDtASp81*APbXAR0{T^1imRIGc0W? zzHQH%qt3W*G5l<4Y?MSuKveJ1cfrD$y|0{-2APJaPb;6mG}Y05`?JVUR@%YVa&`!! zK2*FVWE_`GXBUTe{aSt~^g*wV?TTgXt<=K(CzRt)Do&4`4+!WYb}PT1wj3YzsC_)1 z?|lDp%kk3n@Q;uE$oChzF&i`jut}$NPbTSSjpa7k6TX}!uM>x8h#^bvi1`n~!+ObC zg+HYDUr*klKJ=Eb!<66hoH3R`dHF$nq!Mk`*l0gADqHNkI|=y{q5~BFB)k1Sua+cB z4~P4PV<~Srq_IEUC#F?e4|~O3TUI95+9EgbcKy6(ae?gNkb#E|!;?ed;i2|7k6Q7z zX|Ov4MLuoR+ui=m463)NlIUUZ31xfN@*{`6!4OYaIhko+kSEeim+vPljKahxjWNt#PN8eYwpT9S>Q|oz55;?=);;^O%@o(!#B}9~@M_uwL!C`1Tli`$%+E zEwl?&C8%zps#$5XqR2RVl#U1`&qM@1AlW3ddwkWr70<{-t7^YvMXPU5ShQ_^zo}Wh zN^u*N;Y7%2^sgA77K0TzVXSMq_@rE9&q2*dAt&#;q8i_>QaoWhOtya6pVb{X+W8 z&kgFA4au5;)I}YylbD)EbtsuU=?euV^68#D&PEO9R~_HVU-93485v!8jJ?TSEMyyX z_e`f*ai=;wEi;iq<%~n5IpJUd$(?yEr}N3o{Db5xU5oo>Tm)$I*RjD5HFA3w;GfQS zNGfhU;~NQs+gn&knP1c`x^R`EMVt~i{+Nu`mUX2q>H>*y&kaPSB3y`S}8|ANGYrvLRXJQPlZdx1bw zZU4W2B=FPR!a_?+3+I(Ot6^toOfotC3Y8g!*0CvKA+#?9$PoV(IGoRPfP|1JBj9Q3 z8N$O1uy)EpQeTpVZA{xVWky7>8otDp+`~Y8niG6l;LXZYxxlsn9BN9+#}7W&*Vk`t znO~|RR@_1Zd&Fv7w;|U9dIv67Iw7^SwZ6W-Sy@>_A_!T?@`Ed$$|?N8 zym#2^iqhZ4I467p1b4mu)$kW>UQSnWK5^#$F%$DF6DY{ zAdF}~xu2SU=I}{Y2>L(gF8^Fy%EiBS+R)NR@fP{oq zm}wxDCq_|3L_|P9KtVx4QbECXpZDAmhtsj^yh(G<7aR2OSdo;6h=W^!gO{tn2(c%s zdoZu+N@99Ujl&f}?b%*5C-Yfhuvf7}_Cts@5ex?7qGmI^xjbR)A6=*6=01hGw~3jF zaZBqE)C}aGIWR9~FCOTJeKtV_s}4wlV;6|a{c3RaOHAjVSQ$0(&0ZYInVip@c)KPv zi={j|@czXvLhaL^^V8GQQ+?<*Zg_#~KwK?d9`+lK$o)Y^9W&e6|#8r6x zRZ&2ZyX3FMpd!y#)$t!fz-kcLS3vWE6yq=7o@_W*_ z4Rvkag)yG4x_ya=P)Kj|t72W%8#-0+C~{lic9LGI`)%zE7@DYq@K+Su5a7#JaJ7Gr^g+|e0X!Tq?nxD7{r-1BZnXh%Pv z_pr!Ge$`J2B;yXBl@J%-@D^A2xGVju_PusPHcHU#${gu>UbfHiBgD6ifD~k5K>fmD zcQ1gQ;M7WfjV+~GZJrG0zV8z1(%Ib&mhcY@q>%gl^XDf+ittELNqz0M4t?#ujxRLK zflsOGUdsq8sc+TU->+M?W+FG^sBinZUwS6DN_Q|0}Q6Y)mg$z?`V^;F}6a5iqiE`7~$>))gk;QVq zr>7H~x4P!$=7POv43jg+kS6}1T&KS@%y$cSH7P3CTj0ERrxtpcuK;D#-b2`z_$LD7 zfkRQZ=Mz^t@drT~Do34WI@vdia&eT~%h)4oP$(OIroO)ZeIz6?RXclo`&AMSG4f5s z!lNtH=^4L+>VMYHF`2{L8SXYr^xyCzoMS33a4+hC#if@e7NW-}2{Xr3+Af|q?>oF| z55-+Vjmf^#FfqxH{1F<9g@c0wF7&scb-3`5&_mW!Ub!H76?MUPW&;T+m&aA&bN0ga z!?a+{9DE>CCCt$gV;7w~&x7Q-)~{V-o1PaV*il2?ti(sHSl`5eP$`%)?LreW!W?L? z&1m?1IOGcp&L_1zkog5g*4wW9uV?Suc~A#2Z7wY#*MS3kLf}2(4Pk~p^jf^5LE}>G5Vd{s$@cE95%JOCU)-)%4MFp$ z&aTewKCcn(0lWcO05_59#O9Mp%N`8e=0l*o;A>7dv0UT_v@Nd!KhPc?<$0Fqh;jZY zV}bj?aL8WFdwc{|GgB+&uT|Qxf2mcAKl1XzSxc?DZMJ!-eY`0;+hk}Ku{;5AC{JYX z5`A5rVlTvt_*j4uD4k#tlYGymV>z<6;@KJ;W?oD0rFb#?=kxufBKKG&w9MLObqE@+ z)O-RpZYS72Xi{g0csxa>hMDu~ zasd$khV_73+E)Bk2h;h}#20*KP-#(uzaD=I9v{prJ;EF#cH}6o2Er>v!FAv$XI$d? z-rUy@9Mr-ai4-TTv^vBM&?C0sqEDY9= zxQTKB7!>_J_!22nW9F z1uVcT{&RQ&K6)K2C9(R%(8?Bf7EPTNM4Yd*7H%lL&KnC%*o!p?H7xKZKyH>$RkB~F z)(AGX9XvXa2lgyVitRsicYMn3!Q?WBk(nE^ zX$=VmrqV1I;5~QE%DqANXUH$WvcrPBq9Uiw9L0Wiw$c?%_w%v?3B|;!_`U#VYbf;r9aiR{0Y#G{=+mIErOcYzYgMQWCQI4$D({R>VxK8R|q~J5SP~)kZpmog6#9N3+vJe_Z_@H)PG8Uyj!fJ*mYI5Nv%Xw>5Y~#QVy|!!@JuNBE!O?*-Uy z>MMiEGT;0zx2ZHjn^008jMn(5#K2owQV=442v$qFj-76#DqD`;Yl}ci)ps7Oy03k+ zarncAp5kmRcbs|}nw}bkIXb4LQ!&36h2f7$Jm3y_RRH4Mf@h3orMga%hUlM?!+Rh6 zdF%(-ve|xYrkp+dSajsD`*Q~e*0rMd0g_6%)Jh|754JTYx<;mxg&%9&NFZbC4y&olbY)vvu}sJ#SvW-Wba4uC`si zzP^q=OSa#)8OQo^M{159d-9!@ z2s1PDTUU#WEM9R3Oa!F9jzB92D$ZN`on|Fy<3V>q?;@|D&_&nV-o7@NE&`x=SXfwe zbhM<(k;>~{{?Cbuj%18RiHdR^mcMM{ivylM;lVqKqoGCVX_5!p6p1#rFdV-61w&(F_`8ZYx;dvhy0ibbLnzk7N#n>=m+tm-W{ zEnMRf2l{fmFT-9=N5PHUP;vbG*Q;N@&1?5+d{A-Va}5dB?!j6=KgKxJIqu@U*7a&^ zydBDv=(-Fc)U~nUcRSygPU1r36jW5qK@7$(an2S-8j+Ead0mca7_Qe19*TB%Gc`%7 zcQV6vrz#p;PRJ%hEJG7U1`BnXq_NZe&$iH@CLX@!5BFCd(wrg&43s zuXd|u(9*K9j7Rg79zJ~N{Sb}IOB zh-z@onHu{pg5%?3_a!BB#B*sg7fx0*(!rCPb^vofiZ z@XatWJB^n+uXFho7?}?zN(`u|sAlV6It?yt7tSTSGb!ZrJ()x)HpRi^7Gj5TOW{YK zKn?-*_5AVugyyTnDg5d-slk(JDU+=GxV)UT_Jq@M_m?%n+mE_K8Dg88n-oK#$n&+( z?TLG3#=XxUJuoD#Fz5)jU2d1-7GEOLw3~lHuY7%d)W5Tk;&HjTUDV`p>h|4CPcQ5s zv8Lu?tUzPZJCxtu&CTU>XLIx7Y`o^McdWB>J$X- zoYkGGvYHm|yItwRT-({v3gx1IPA8g3f}c?Wvb4<1Ov3~Q!>;G2xcv>rQ$_?4FFEZD zRaI4uj40JqH8eDsG%5ql*8eT(6i$0%V`D1&2%GyWgXAHk&J>eqSq&w?LoP)}thn?A z)QX+4O_!Suuk|M2ym%o5ry3RF4QD1ogTHRH2$486Io=o~4^ND%sU+f3eqM#6?eytK(8k#M#EYkOdgkWw;o-k5*H>1K_vTXw1)^Pg9EF6CuZY)~ z#{_1)CEyZc<{+6d7@Jw?|Ad6X=XM@ynGhRWEOSUfMTL&X`jxj6*sSXroDc`6qnu|s z#cICMJ%oVs@ch8Qk;14w5O=TPj7V)LENF8$J3{Tv?o4)U=j3D^2~=5ITl;9epBkUy zk8lUr)(CB>6!tcNwEa$Sw&6ZYQC_OdjEvTaiP*{iD6g$;zi2xPKi0$f2c6$L4LW&} z_jo(tLl4w$2a|mj6g;45-0^-hOwToSTz4yi3Aw{XJb-rZ3@b1biERxoI38%ZYKDsx z78HO@@|G1G*<4;;iiwHc5#64>Hh#a=9~ls6=qa!wJJZDdU*$#buW0R4X2-=~0KSU_ z;kyJ6;Z1H2dl;m{Z$DtFq$h`vZGZPvcI4u+ny$=zD9d5=hc7&pv<~{Ft?IV72Oat$vR%Q&{n{)3BCB419ozZ$p!Pjti zdlk;<&&AEHlv_qePfsovY8m+Q&9CwCGtav#;z_Dk88^6h4bkqBOH&mVnAFk~`Q~fV z6&B;tTTWZ&Sh*pP`N;D=(aM^l;pGHKM0zKA*9uMPPxuh)i;z+TB)RmKf@XT&X<~^| zJP!*z02WYultF}z8}bnLa=xNNX!mgE;z&+dSh$D1ii3j#cC^;JQg4YGKuC3B@>@t) zydllu_QrlCp=MjGq9zmym562_?eYWW+O-LvscEr{7-i>HYE+Yoef_hoZMC3k zj%<2G1_J{FBr;h6d)gdMlBIekCMvSBBv?;SQBfxk-0UMhxjZ<}YJ~}#^UOcMI;rJj zl#|*$iMrZqlDMptZ-b$Lt7xt)xv02_PP*I8*fS0Cp-KG6CN)ARa3Br9o>o`ofXVP`_MlVcjNPT{)B zN=ib?Uv@n26gpjOfi=tone@lhfbhD*Ejf{U*t@l!;t3zl#;8FxyxL!EX>V`8I^8v> zJ%>ZRJ1F=W!aMTv^M{9q*7}mVga7T@@cKFK`MC9cQN$ob;?b$ZnvigYXO;kaUtU-c zfa^+0Nx8UO#^`u(0@RQdQ0sd^gl3LVtTwnrdnj=SP})onZQ`($6d59I78aIDpEmS} zOO{nK@=4->NLq!QGwjlHW5$znH}8u=O58h=&Wq;z5HGHXigTc0FK}g;f;C?@xiqQL zq+J=3bw^H?nZUNk3c!uoG~HCLpf*gPg@L-qr`%k#Z0RK75Tf1HRUx!gYofOwy$GVW zj6{hv57%-PvZpGo@L#^{tPC}piD$ETnnI4hvmH6nP+C^zhlXEizusqZT?U20Ja;R` z9iUK=AiNjY*zK&py%`Rbf2iu|>gww2+by*|KFx3|6BiYATJ*t*WkPnjJf@v&bUxba z7hR|uM@;dbPipUDWne6%n1Z7*=-Tv$p#&Oc)4T54VE>X2Fv zTGp7)!Nuh=SMN-5?6@;wyU_e8U!{nVhDNtG$0BxbFx3~~$GG|{K-kEP(I(>$-fO%H z)}&Bm^5)5-NaJxLS?%ID4V3rH0uS*Qfe+f1N71W@dyG+f+pNil^mD{=QMoGoS3UPb z*i19RR~+^l_r^Ut(7o7m>GK1V&)x$F8>J@SNQ9>Pl?Sc>)S74-@Kz6?P7YgP z`bBPXZS&co;9Lac5P;@46Nup9aUCohFTYl$)nPzNweq*&-^YE;V%yzi^5jMU!IOoe zfZQO^)h`L8s8KV|1_)CZ_eTUHl-99*8?TH;cJ@~8I>k@MbGEQbN6Thi;I@|w#C8rK#SV;4#nh`pZFae8CLV52D zQQF5m55Ela&+}(}$Aj{+ntzPQT?6qWRc*iS-iSH~{yz=)i(b)XatplE(wdEarEz(( zJq~Ue;F~u=qobo1|?|GY^pMSNLU!>jWcF_~Z^4|C9=0r() zT-@Yrou_ANWnv3J_#7~}CLoV_c^iN|tgqhOj0zjVTwSjgTl_$T{T3)ADQP%mVQ#Km zpf1#o0irhnae$%_t+e>jrVSL9!aMt2-HjjCm>;(1^2fIi{{!xJcX!7*GCCFq2JEK{ zCMLK=L|&eKwRsJJn6fbF2)NH)-l}e#zknJQtfL)^R{%ip7?X;}X`h6f+jaGKlq9*@ zGj&x}Lmm@rYd}mYXC?hWHR*{Jd>B!&nat~2>#(VBWE2%0-9AIW@8Op28$`g#%)s!Y zw6wIkdSYtIWwO+02mUt-yatiIJqW*}+-ABmsAsDyg1YBNXgXF7Gt1tR1+k7!g$@JO zpnKl0GyC;Ayo*9VBouVXcve~<0h#Z00h^s?5cfDPaiN@n3F@o z(Ulk0kei=Bouj7-+&J)`J?!&N%xG}Lp7Q_l`KxjG>RUw^^ZiMC<|}nXl2TF%^765} ziHV7~H&-mvn(|CUTVyMaw)T-DV&XThtsH<|ERTzv7l6EC)SLaj|1JMZlxNcp%F1Ag zK}Q~)%WRJo1t^@9^;T79SRYm>a1?3PvD+?vaCdi4S_ht01UNeRvNT`8qi@?^N>X+n z5tWs5RfWP8_&7b`JENU2-q7k_nsR}aR@2P8nmY^a+F#su#h$4PHT&QR!~7B2*&#Q3R6=BXt1!cvDvG+aXr`j2#vXE zX&>jyQ;N;l4Z-#k0ZZ-R7k+Rvb5SVVxUC1OA@@IdO|A_u2#MOs0gPD&Djl;6ZL}wQ z&*pVOW^a6M1ljt)5UkVCbjvGOupuF&1kx8?Oe#rH35n&#A_D+HcDUuKq!Z7VL-?6@ zOUHfC3fXcKKt^@Azn@cBNM(}34YT&HvzaVy8yY(7vNv>5jiQs8GGk&&-@H)S&`nkR zN4wuTZ2VCpMuz?Povp2{va+%yw~9B;B1nsY!drSz>e%+Yyga~OzvL5PpTJ@0(#B!09?P)4ytqIkC>l@l9HW@C3H%840opeF|SzM8HWkQTmDvB6cD@^sTCB4L24O9 z#5>j99bmR@zE1{TVr5|=7kSBU8Qaj{?v0cEAJtuqxt9~5kr0fW;O;B4jwDK@^7I78FQDKB-sN%`bu~%|5RjL|ooZWMTH~=BlG_m=HWLK+!2Ta5yCGdXdm) z5r%<{G(fcanPL8?efU}8cW&@N>H#1@e2g?7kz3UH2WC5@>(%BjH@Vygg@7zEM@s*CEvwo6Dh%V&oM)?kOJZWMw-p6BkZh8Av%Y~Vn{BqD;S zG&23iaFeJ_d?6UbEAOjMT7Jsz6ZbO-w@JjAF-b%|Admn?K{ zv6!u$wak!b5hX2VAQ$mb>?m!5ntcT0f&@|jqDs)@;#oXtQqgAQfCHY{2cd2B+31*$ zj3#t>TW1p;Vk*ZmM)b(#XgL0z+cS2+xzq!`30mk;xqaA!rvHr33RhrvX6EDD{h&6s zi#BjZ2t*Wcigb4C)YE`>{^{p^FQ-6bp1AIOsspfKe?1@&d|%0F2XAS3K989t1P*fL za6nTwdtxi{3Ppn0Y6`K#qS5lO3}5e#7OH|HgyIqIn0!jijv)saZvpld8recS!DyAe zO^_0SHFNSoYywE>>zOzRiJgGy5?9pF#NOW$$rb5Gygp}r_p$!gfO`M22iPGGID;DC z);^+c<6B?=CM@;w^D)9FE*551Vw~VI!4h(*Jht6ufad4rTO|;%Z!4N3M#$2c67lQQ z6R-d+9A1V&Q1P;x3DQf2-SKK3pIy(VxHKLP4lrO17I2c`AqR0>iLVQBA;VvV(m;9T zgQObykGwxwu=1DSl&W1?pTH@R$Ki-=#DR>t=QA-=@42v|h6r?V7SwK2E5OIXI@5m7 zJ7{0x5`%kV#r-BPSJo5|*A(Z{vHTvz6D|d|ybB0-e{yV{OE zfsei^y-owfem+QSxFM-8a%pKEJDsH0UG2mr^l{mgOoNZABXd5%O5O25RPz9(39h1? zD=N+zP@90D?`*E3Y2Yq&HFaNtU(BrW;NY*%&= zm+=A^qZ%9zd*ot~n}eq_@OU5|U?DR0rl`|N9PG$KF-awIfA?8)zCCLDMN-C~-fVhMLF+WhsCNcy{`oPMV>t>!a~CtnsTa*rW-D=Zw_EIiU!?f z6o*xt zV_D>ei`(3$zRkB^au~H48b7^b%P#fq^NnMo!ciL7pfoa**{}d5Ypf@nPw+^c$5Zq$ zl|xU6fiVRM-aZ%VofH_WJIHT(F{T-z`A52}R3Wu?C49D*$pJTI3s+fQUS3N}O2-pi z_M=WYlA;-j$J)Zf2&x=>KSKi_BjS%JyG~)dXMyk7ntl`eJq(O>zs-c%)S`e)n@s8+ zga*kDt22|TX`*iL3e?TOy|Btlf3&<|ksF&AWAv1x5mFK|u0HeHEPNQT#3Y1@=&s=T z016~+ph(I2wG9yK_7=->wS*8n2?lh$o07d$TqjOq@kyoFh25FHo?&w-O-)U6bGkGv zh?hRl;1+pHg7>#olAyo@E;kGqQX+Kj`P;-$%@cFk&5aFFc5{b{M%Pxw#1rB5N&`0; z&eqme65zaOu)$TOE=&;uB?k<~pI24AIz4+r{nflkg3(z?mTO29-J&?Y5=TL5Iwq;0 z(Aw|zCdjxiabe{+LUd%`kKng67pI?@rjoHpC;`+Ot^&BGub?r`--iIX^bvjjjnq1? zBN~Wwsbck)l0k7z;&u)B@$!o5rhT^Yl}vRi(34S-ko%gNr*UyX#pMJVN-yX+Rkwmm zCHnN@RXq%F61S~3S5{W`_K0zCsQ-8*4$9QqKr`_`!OZYm)$J;vyMf`dido5--ET)`{%Cb4s` znEPo89v`G+THnf?kFa863^CMZz6LOqrJuIxFbrO(z3&NI71#aTG7j$G#yyNdy-vN zW)BkEckkW->M}X}K5&>TGz(Gg2mUgi`BO{Gz|+QOtQ2~!XvOO}SvomlOM%f@$y&{%5W{pQBplA)-EAPs z9LEfR{cL`&fjGQP(V=|z>oe~-D^3||1J%1*;l;&laf$R_ziv0ii^kgmIt=#?+wrs5 z*|(oOArlv86cN5PGvkali#a07{ULwHBVNcg?e4d;yE{8GLq<-nprCLNi3Jfj0s-EE z4ViMTeUBHd4&h-HR<*@R=%;o2i!oLNnr$@p4BbDS=vw+MgV*>jt z%80we?wZOfsm=SStVme76E{X!j()J$Z z4#cMqqihHd;XSoRBTujF)?7bsdIle*SMlg%m5zNKpWzc27*J6$PDgJrIZMeA`}|2! z8DejW9l=a??;q)iB{?gO#Eoma`bf1#gDdUHyPsE%)esKSC>jA_|7)VRKE!`4;Z%0Q9znOmaESs`L>b zEynvmc}JF69| zU%wBW!p*e@3x{V!5=3JM&cn9w(wE?Ev(@d8evKY6zMhDe)Jrh`M$@2F_E|8C0tw;-3kS?`Wjm76yk)HA4|7 zpRC*|_??*^sJcUF9@}AEODSr=4k|*1`_BCfu{O`vKsIb4i zo6}BqIpzV8T|9KtKJJI9f=30Zga(#4n^0lZB|*ie321yl#ML45Gjwjh#;aA9l30)e zP*%f=Z{zM^iJz<&TVRrDNdm{ms+rre$4U+X zM`c~1v7*HGlhKUQUly%G@)}yW0S~C4&k-Yo_d4a&w?HEd=)hejrvc$y}Kal!m zqNjx#T@?v|7$e`%$WGC3X>3Mm90>QPZb zEX!)lzHi9y&J6-eO7uX%G&aFdb#IQ>^<)da=^Sdv4gz+gI~qBev;qq$zN2HEF9yfv zNN#%EkkFsXZH89z z)@f(mESqQ35hDA-*>2?KYL3^%!JyL0a!e}zSXn(@Mo_RdO=x($K!XB`gWDoX;d)3B z+E8OYTEDc^oh@y(JEJx}<-%8XGk=GkjxND&+{-K@b4E#jwL7g6h<{eQgJ~NOuVIf4 z>PlO{r9_}m(9=7u{>Hcn?zfhOMY~4qIk*j{_x*ZBo=&vhe$)C%^%+?qGnkmu zPSkXx%io_~NGPUrx!!WpGPmX2ICJ733$!{|fSrX_#YC|FnYwp^8r9O356;w^>8vay z_#D)vvaHPe>Wm}2HXu;%{wHGYz?q(Y3~GEOrIMLlbVBV6uwPLJy-ENh9H88u*i&vGTFn6ct$R7K9@B;Qi-dht(k5!LG%-Tvcx#e%F5OXu z17)HJ_`(XnUV{rXhRb7s#6V*@>EdD$3LQs%LUxYc{V%<$@Zco1C`_gCbF{dGRUCwe{tb{s z(mK-(S-_$55!HPC<}b&`KI0g~w|yfH0ssezgb*RD)vT&S4Ojw>Bed`TmYkxL)C>l{ zo3*8-C{f&Uho>%{&AcNU3%=kf9=l~~(rn>FnO3a%&GXYVIC9m@40`?P6n$+ip>Pm0 z8X7Mxw7zIB3|vmR&E2~ukA_a?i~lK6yRN!B|M;>nYhX z(Yn)dto-fk1UP7F^ZJ(qRyJXOSwwh zwYT>L#beAnug+*~z3+X6I~+m4Fxkg})@a62@Q)cmp9O$qoZ*Iv7-aAx5S?Ig@!Dof zYN}*n)=pJBT1`#2$%%>3N%-{m{{G?~5$}@)^w(^JAI}Q80e4S0)6@jofP&kJu1~F0 za<^SEBlu?C>^$hyeQp-JdwBR#YZg8HO8g;GZr#3Ah=w=HS(MxF{1W^0wf{=E{g?tp zqVh<}+h$4%F=_FqMJPQuy%n^1MQP>_PzPIcOgbI?oktDcy;qf6wYt_u^ef(jY<*%9 zp`@;RGc6{DGqKLv>07YdcYVEITf2>n{A?s%gTnvpfO95a1-7;}@EvJr*u4g6!n+tn zMv{jK>dv$tK_`DkneT>cFp=Okyo}vN7v#d{&mGtx*6X)dZwK$gIsr=8w^@?e>?d*i zpB<2!b!>uGydi*e*w5`EGgnpDZsZiQ0*&Sk-ebf6@{zJ^NP z7}RF)aZhydFo$c~Kg#c2ykqb(~XCwX}(n99bJp7NDKaWy#CgRPC z!gF;OFTw8&kQSSR70Q$x9aJR!f( zLh1RB|D%5fYQms)G6;k$4Ll~6c6=-lB1)ZD#YDa%&;6Zb8Si*PlwCG%q6=nYc z4nNH$e)GCY(bs;{@q@&FS6a&fH9r3j2jROkY_xyxdPo<{0Xc%^90ajrQw#_mEt+w_#efBY8g<;*;22vEgo=DW2CQZ4(p4QVHx! z>+{+bpx;lsVXH~|n`N<1kdSX!%PD*|!ok^vXr`< zqp*7J$B+9~^i1qYCP`3goQHkfos4FX&F}a4j`)S)btOHYx9#)=;d-$}iVM1BYRtX_ zoKx5PF13FoluMVgQVUCwyafcLfRJ^5v!Fj(clsHl_5NfHnK#oBSiE zT3*qiYs%%e)Tk`Z(R}rl!ApBpc|-3+p;M>-4S3^(@e`mCR1HzlYYMV|l$Z`4L$|SQ zWqOmp*74e}eW8}QAC~~^jyjv_ETG0hqsrqaI{9Ep_4#40f(AEijB;4F^6qM0FT7(t z;dRwHoPmuv7q%Y6At`pQ_wWI7lI~jb94#*FQ#_=M^h7U1*e<&jMo{|3BHG!dmg%wU zbXCzy4mk9GoO7$m2?nUy7myKEc;+Ce*c{Of zRXsea&o9b&|HR}gL~5ngm}}VcNttWpC-Cdj=`4WASb~T>-d=1BO7#Rw1_ahM6Ggi! zY_*66tw?{{duvEePaiwU$P_X-x+_2^a{K$qM-r8!vaYM(qp_*Iix&vQBNv*^D&Ix; z33RxF!3mHw=9Glj-7igZcYeRuKo8J7vA%wWKP(!(X9%H91Nyik1J9krw5_Vyi%Nszaif^y zLTGZ9js; zpPY1jQ4>R5=a90(X*D|f{uC4vkC8!%F6a|MW*B4t%*S;TZp77SNa@k44Q3IPE;8Dq zpfO8JI~r7iTPUM99#&c*j#v3;L#2?_1Y1TWVATLe*&l z?~Z@mk6*L17>Y`5d`=52FgQhJgNckgJ4@$FVfKj@={V+ScLR+1^Eei2kX1Kq z<4GL5FhCthzmL97^qp$S5~A-0K%kD0yJp5nclVIcHt<=dV8Gw!UcirY2Pqr!AeY%*EY0zWW|j zaHz5g4KnU_)b^A|_sGq3moC{#Pni{gBK&M^JW7Ilboq^<2SaiOC;&k0B7M6KLrz^+ zk!+Jf0W6c+-AoVs&R@ zo--G}P9(qDIXd#$uLXkX-h>1VL&FRKlGGxJi^yz~EFxZFy^AO?Y_~6#54w}-q}(Mj z?n4I9JLT}mcgt}uGKBzecu40OYWuzWJb>Ip#aKwR&`U1ISb3?Cm(i-Dy8en5Gwl&I zHFbD80RaII4-Xj8V`gF53@8oz@=^vV`w6i`s{U{2)*0DadyonF#{Jc#1~r!$9)tgex??p)SX6 zE9FH1o(!mh{wI!Eq<&b+%1j@F$1WAm8y|DyQ7obP8>4yThJzJv@R*coEmKNFI@+PBzp2n z`_N3UcRK!A*1BJ_---C}6UstNKD4@Ikebd5+SgyHY3bB3T`j$kgQD@GLj)p7Ma;Y+ zkxZBxnv$)NA!sV{@??KJT3T9ili^NIxQbFJqCH{ic=tVC)>lE(xInLwTAcYqNzn#5 z4t~u_qn887HUk|ucx#@Mj&1=f`IVSmY&OH;p^_!j-EgO4I6jU4qTEGy( z`s!+CR#tm!tCOp1L{#DKCco7gvE%ikNjC8rw@roZq$8vaTMRWkh-RIfT8p6HZW%Ko zwR5!<%&mjYZYjpsc5z_=L7H~IJ(&Gvq&!NR=lH=WS`Usq<=$=n+k<<&k01i}BqF(W zpiv=x%`!vdUaHc9Fs-MjXX$lCRTckHBIw6TPbXKq7a?pO^&<%|$epdN)OM4h0e&_9 zovR=4x-v(XNd5C~QFA{RwHK8f;Jyt|{KbKi;@c76Lk8;nUM}y0vM5?Bv2EB;4O& zWp0e!WF%#K3wthE>prV9yaJ=-(A9Ha9mv4bPC4`4KAFllM;w6n9jJXRCrX%l~cC$Cz{~I*ffuv=zd*hx!0k~P(HG@lW_!EQ+yl% zC^`MDKYNnLJgL!T<>eDoa=L-<5__Hz^UVXZ0@LArj~m71u@UM4I%-Bnz#!;BA4tWq zyW|`&O;d(OI$$29Xhc^v8dYL|aW|>GQpiT$e_9ITpx^vf{f)OXzFS3 zync6{F$%=W)PJe0yRD(1Dh|A!EY%7P)M0O-W&?vAq*)MkP&LRPI}p=d$6>qTex4)x z!{y_z0y^##q6d)F2jHef6t$K|O@tBSXGmbQ9TagvKVsg~s$%+p%9>57r|xInU}&D6 zXdMv*KygQWm*?%G<7NXCq%~fSpka#>j6yQIrx$wE4$n4|P3)8b#qSU+MvteQ#v;F-enoK=T-f+mJcH zZ35e%mm%VtP}%0Jn0l-?ipQsH-@!nVJ>2U)NI9Z!}3zCCZ`+yF7;ZvO&|k0QFO z$sv%ph<8BKQyJ4PYD&4f?p;xKHfVAKNikgn>|!mb0!1|S321r(?Pc9WhKM)73)pJd zubFn~s~tPQD15cFD42KlS_l17Cw5Gftr~Yr;6I$$D9AiRPtNc8oh}0L!hRoYE@r6q z@#5yPXeQ`({|emargeQg24vw5$XDVSs?g-8641Z;aX8^6pRLkm%3Os|Ch(vGd_Z8| z7*H7Al2N=W*7l1mCRfTAM z_K6xrtbq5|3iU2B7%Amkw%uZOEZihihBI~??x=3&pt}h)aE*vqi8){5rhG)Tc%Gvl zW7+@Y>reWld*y%FDN2xa{~yM_GN7t;>w2Ri3MwifNE$Q(($c6X-5}B;B`w{d2na|w zNJ+PJgNk%YD2;Tp=}q&^t)6qwJ@@_b`RnLed#xwuGh>YLoccl781J_3ww-@%-bmWa z5puZpM@;CG0~W_C;;h!rI=cbxfn~N|{Na_ZD(6sguqK4Re*Ajhh|TNJp|<{>TQBaP zHH?5SsyR67!)c3lKmn0$+*^QNP*)J$i#J3{QtGV2I@MOY#w%5UI{(+;-o_u5tSMQ~ zuoKLNxxLlmJI&yD22p$s-M{m4W*Ri=YPHG$h#mzG_5>!QXj)Ueyw)-S*sebd62^I# z)x+at;AI!RN&7?IFA3%&N2@pu0AGXH<(j7L76zDEgqtUjc>;aHYRvFC2NndfgaB;T+dBUbB}Ya4yT3r?*9O`j4XpCXlH_?mP7LSE;2T! z&WzeUUNmR=FvPPzc^a|n25)W+8|@MWxH6Dv0cJTeij<9x7J<>&re+XJY&90sTNY6$A~)wDY}T4tEBQ$IOQ>Mf0p9zKX%&y3dw->JWUm+=(h z1Pb1{BtA0WE>p_+p2^CVuoJi#m=60TW}-u`jFX(Yu2}xkpeU1r9Ic+>-c+~ym7=b? ztyL-1r}lQq6gtFDNF6fDMMvA&)jj<5787J*u9h9iL45(*vUuMyp!Iw}t{ZjhK5t6{ z6LxBKR`UfE`Q6d}XY$}wy%GwFjP$0(L|lNm=k#2sTmN2HZkIqRV8{VN5{X_ZGqeTw zUTihNko=r+hC;IT>oO}|HkoV=z)@#rW&)t<6U_MvB>(}JuabiITXmcQfZH&NSdc$( zzjljC1Ttg6TFR%q@j{H%{Tax}$XXs>?;luad`isT-}vO;xGykl#p;h5d#6X;_AbTX z0df^|6O_0y!LZ3P(s9$9?b+HLo&PYj6oRAPAJ+HgMoGbEqnFG^LLO5y%yB2_*~3Y0 zZpjc^-s3)^yvlJH?QPk49}Un^nj#z*1-8;fil`p5Jw{kg5;Mh}992A*-0D|?)dQC|+u&dwKb#Q!;2*wt?GCUs^Y zE9+^?XL-9Y4a-Zt-5VrTKh@18z<+mhJ1H-}_ew}sUjBhrCL&P^K35I_N3XOKNG_}z z-rJrn6A@RS=)H{)YMCchQd3ktVR6Ay+tArhp*~X@Y;uoh^*XoZ+dMNYh%w!cO5%8z zy9CQZ8+TT;h*(p`2aB8rYvh07#&n-0rl+HNmIr4mDzw{^$}@`W9mt4zZ7Ht_;& zEyql}3S$8Q??DYde%#vQ{cb40QhEgtP0rV9NH|ny<;}0HM8fF)UNZuHE zT==c7PR@Qev!^HH<3~Yss(5{pC5_?6Pa+?#5hh#a5UvLgWu7Ri)BTMslkaNaZ(Ez7 zzIl_TB`Kcoyn6Y|A*ew|GhG!NC<+p6(Sgr1cE0DKu3_M{jI$EtVm(`HRRr+0P;U_NhbBfrtrzz#9p-FMF??aGO9y!TZo$1t8SUsyyx+-Q0_SLfg{_ z$D6%VQLLmQcJb=S8%Tb&loH|#6A>N8y=*?)->^z`dAB2)tpE}^zZfSHWD%8OoNvCk zx%8YJPR$<=N8XWdvhthG0R95&u%)zE+uJWZM^X9f zUig)O8_`hBY9lqKz9HPqD{pq_Z_e*K?&v3yI zjARxs2V?E7uh2xg-6Ij_>&s-JCi={Ny3enRqwnP2li%AKoJ<&Gs?HM`3p3C^WBj1s7NQ5dj=dG2&!aeE_^VeLB!e?bNA&0&|9* zV0-@IDrD_vlQw$|t+~$1)Ks~4Tj8&UrAGjK=V0#M z<+6;ROT$Q*mAG@gF5e~Rs+SCm9U?MFDt)Js@*hAAyKgCpb1gJky_D=D&{6=rX=8Ko z;zb}}rZOr2!nGr$0DlBWEpyk1?8#OiaV&8CV0p5~^UTs3%(*=D+N!c&e+n=`x#d=6*bhk`CmSBad2oaF*X2ZWOHe3)Yi?L{x0Ve__%$~ zq`x|SApH1P<;gXvV@haw#gwtpBr7XZyx<9r55q_of8^h-hbbBQff6+{BO06;C_sv7 z>J52#xN+U>=C#rn(bU}SO;4KEW(TC(f>9>A28L~kFSMzo%U4sg_XdcEfRE<0J%6qL z`!}$ZW{XkDq8jDTbo4nNXw@-f7U+mnR#7~wvc8vql45NKbXhrXJWomZp76$Jj{{4= zU_ODd1xTuEIT!G^ka**|HI;_q1$z_&vcnMSFu^<)&#eG<+OBeZXvKRF^U#Pd8FI@l zV!1hup?fduyp=oY)-U!>xRLJK-gIdB@?7#qUYyg`Jf}hDR{#py5{5L1+AfZ$MDqO% zrHS=?^Io?~H3ba4JDli=w3Gq>FTM#UI{npb(Q=T*awqL$7ipnwdDB}+KxS%e#3NKl z*8l!bpWVNJC;m0RzydULAm{Z&s2$*7aDX2MCCA8v5wCL@E~)05Z6!E>V#V@s*%QkP zS={KF6+TpXsIELw=3%d^y>lt6CI@VCmkM?pu2>e zPlV`e$}_>T6K)dQl>|KzqHk0E6H^tP@{Kcxvddnq9Q33JGcwjtMTj=nUI`wcza`xK z;Eit3#bDvm7M`h-#sz-Vh~2DYY3O^E+Jrfj{g^`+FOr*WEC4m!BH3z%vueUms=F>X zjb+8^{9#(DAC7%{@XS*K{&dxSR(P^^N+Eb|EJ5m2f1D}#uC^fJ5kLTK?!Y)Cf|+}J z>qNh`=t zqMP43pcT7T@;Fy~uO-|>SB|#8H|J9fX~}a46@U2)!pfWVu47l`ygIQLSDo#ezO}x- z<#@)ODaop@CXNGnI#G5}cI@P70L;n-=3~h(Ps^P{RoYulV%M$iS0heFbFbH z&H9zNHU@AJR)wb+eDlVzK7HgHO0a>oSGG9TNs$nbdXV&$zVMW>CH`38DfbhhfGpmm zS$v~-{bUQmoP`vXin2)LI`ZCPD-FX&9u>TxnC!^cqYHYN6#sKm4zX6(Z2SO6Pt$03 zwStaGGlSbRCVlY52&S=j*3|`myZoQ=OmoZn2D@>QZEN4?x(T)zE?MG>lcAxav?HkG zjFrO2$oSaFNzWLC42Ky1Mvaf0!OCqgAGgkSom=2wvFXY*G6c0Mp{4Hpd+4DDJP0B& zqf-uG5DP!@wT4Sg;SU}jUbJhpWU0l}V+jd~+qaXiv+1M7%%*V3`Ce#iC#9#SKPUQ8 z<6QDt(mwqj_e(u(W zhDW!8gI2b|DFLmJ(-f{QL?|B+a^Yc-l8510=ZEoHr5YeBl9D4jtS@Z-*=%O^|&r^~I zon0^ifP+5t33{^n2?aNe%mS(Eur9YNpxmZh+RXerU ziSvQMHRwv&PZ7b}-UHFGzN`EnphuYd1nEAO_zs2&%lM^d_&jA5&T|C5otc}{*Vb<3 z3z~I?350qZ_$`)}QMMp)@ah#tZ)rgRsAOml$;9(5d`rgarJkY?aBePEx-g~i^eN2U z=VFq(1SrO|uV00C3ZAN{SRAz6?KyYh!h2RdY(o=31SzCTW8soht^idR3lpa2D>PtXw880I$)H?&_NE&T`mwDy(W1{QVS#(rOOIRtrOV7h?6 z;s{bGAQ50~xjq_<#NLa~*bkWFWzGm+>?w4rs(xMR;{4h0%Ai4yHwODP*rudZ`St&4 z_uE4?s?Y|O6|cBdZ4~+>5I=3Hm>}RndG$Q%vIj^mylWd8imARfGc$wE7|+cWv9X(E z!ScSll9EDcd$tx8;Dt-Ey0UUK=EQ-UaJYa(n#1}copkzH(%FJ~xtr&I|LW})OUR&) zNS_@i6YHd7WYint=H@Or?(ehX8+W*Wv4uXn#|`&VQ8cOcL09`Hi7jo!Md7;1J_uqP zXQhB9QCwO&P=#``br$zMNfEo0FEF$}`{w*kSNtQE z(2r5t{ry~=CQ$~4gAL6YNAIq8HZ&;XGrL#f7~0#LEdS`rGF5X?8!p4UD*fGp-XS^v z$pWxfdT6}OorS+LpT-&@A}U@OE}JXRiH+`5&DUCYY;AphjX55A@!ZJh0r51o4dJXN zK%*YZ3?=dI5>VQ!)0vVyX5w{RvY|acSyh(bi__mM6!k?w|8{> z+M_@o$knQ=h3VqwkK-fxd)!@GX0F%D8DJe9ZE2+m@DWqzXQcs6?h6g0eh6iAU!T6RLhTkCc*793m`5uwRQTVDL^bC^mIH7&nq_L4l7H>#)S z_2veXWP*V0LeIiTvN*Em!|YV)&ceb?U9~#1QFY!byF~$|+xbr#?xxCI8n?IF3`mzg zq782xshHvd{~)c1>4%I4?s>Lp>*<2KxS)T_EskV$JzQ&Ve>qD%u2RlcWLo=~gsT(m zLHE(Y{lUR2T5d;~lNW5z2$@9o8zL>btXBoWnuE*+?~Cq4ka_tv&Y-jFvcF{r1yR`i zEzfMd&PqP&X#>$Je}DhnoSYrxcvq*8d@Cq5Non1@xYN`>IW?tLVzj+H$kyWu;u?nI zl{S9dUs0co<3f=WYZ|ct{Bp-Q4Xo<3mq>{S2pZ7akA;Qh+xPDgd?ZkZwcqrEb`Z7)QRn?xf*i)Ahx8crq$#YIjY0o*lr7r0*13 zZ18Klb}g;%jpX3K)3KVRreZ70TaH_Im8*Ge2rWZR#-nY;#EM_tkFAvHZ_ZX?kz5F^ z=c-lXQm{km1(L_jUdm|OR`{W#*B^fHGAurF}h?*N&-BgHwMt zdToB`kV~8mGDr-7qg=ywkLi<%r0=lBVJltty)A~g9#YT&lh`rnna+2`&I26<$=K>A z;;C-O14M@NOLd*aVjja?W@$P)f&N76T(^^V7&Q$CH#ls`ZC&LlF@qnboa5)`-=INd z^i-{l=CmpLSE~}#hY4^yPnnObSz8ovv9K(URynR)Nv!!@RJAWEOBs!v!sHhBC>9EMy>A~JW~;Y>%VF5M{nTb50U2{DLvTM_+Av_T zKYKV=ZEU$?*FL{&Z1Hn(i?N<%A#<;^_{KynMZ*q24WD!Mrkt!zTm0@mpWdBca*2P;1YV{M|d!0X_WYzR%Z=2M^?JA+jf8y$U@YsvDcZ@X{( zh!()sMK8Vk_rpSV(|29*ZA|}i0S$v787k{WDfLK3MXaZ9lp;7yh^;%{THLB{e_P9K z?X%F;yD2?H8#IRIL+lCM!kP?m@4fC)U=VW{ojZ_w-=A$zbQBQaG01q&2g}vfOizym zm;8)O%=D)nyG7lvk1xsd+8z6i_-%!S32Rl^_6<4^p&Y};Jh9I%cT`Hkf|wy`L`|>x zQwX}+tG}ml2mydI7qZ#<`^u zWi7EEMFz@%Z!fgbuOkj%pY1$)W38%UGg^kkvc)04T397R(FOu(Hb}GC)!KI}b~YMA zNe_8<^li2;TJ|+I7MiLZ-S^nXJhMEl83=1-CgYzyL2cWR-2KUT+Jh5pGlYGuzx#4i z%w*zAm!y<&ZGIp-zT0={yfy1}Smin2)5CH?DO{sKnoaz{4F}?A>BXVNr+?y!ZZc0h z%5MtF{VADbBH7|8aNKOmrjw?ASgaC!oU|za*JB)>c2vsgxp`s=!+|%XUJ;|&D1VO% z7yP>GM-aP>d1PeHhY*1aH^>1l&4b3!$-=I(R?)WH*Na5HcDk2NfgPdTHeZi;{=y^x zinkyB1ObRHosiDJD8lb{mR|WG<5(BF+lfPx*U0g+GSQ%GP@P14vcGDvvOIo#f3c%SsxP_rS` zYSPYdB>2-T_gnc9sTA0kRwoXdzRtifY-zhwKXD2$`uut4-v=Y>(f@UZqME|Pe<

^k>)+sk#K=sF9je?TueM$K-d|69g`F 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

TFfVy@v-e@fqj zlABD7o1WuWRT;nHGOT;eZlNItaDI>1lJ8xC&2}lV6|3uh{kfy-Ha$h((qsEqlmw$i zPJj0H+7xq4JUB52JHIfrqg#G&f%HPV{IE_`k22Cx16 zp?`S#C+%|HckB1&UVPN~@U@X+y72+j**~*8Wlb8ipPirj)l*Fn5ykw^EPI2GJAuIf zsxP}6SVaa0QLtk0xZCSzMz#V6Btp;+bt6&v{}`jpi!<&*_&0 zo8j<1*$SLc1-A+NErT5ObmNiRq*uznbw)PT#osHe5kY5LSqP!B|(%P(iuL>rzP?8gNprn1N~LgJqL9NJu@Ct2uCu@luQvt1O&LV zpNAWfcs1_O3K$(R!1Qv#5R)p(-pagi@yg zwBLImblUo$V0Pp>1njMz@Acn}^myv~-FG9JM1F7Wt{SrmMi@~|O%0fS*B+zkpZQ0Q zWnU;<=o$`&v10T`yL?c%Sl@o+d7y2Q0;z908$)@lgTG#fGuu==&*;VUAmb$=C-nlTtPcp8=^m?eHw z?-X8YXJh*gtf&QiXlOj6c8L<9w~?(Q;oly}<+>nSka&W*~–J#m(ku8d(`dl!4{WlUWeD^l2W*i>)& z9EM84PRn6g9{BB{H!wp;CDSa z=bVO1>?k-z9t5o(td2OCJrD2b(6?{MH(QyxM8v~4HZiXfspMEv9vYe*uY*%`@bf!vUsq(ae>W_t(Yz3Rb19&p(pw8zCoZ1xB(iRglOf3(jhib^G? zh6cw_9_h*kAB;NYjg&Vm^qjWP(Bo2XoI$UYEYTXLM8qM~>#=Nd(9*udP6U=ACgt1i zJewV&+g}t~9d}os>Q2`~16B*`rC7_mV`r_C)I7a58XLGd9JW1}m2W#7Z3XRynw4&a zY#*8q8{<=sJRz8^s_`M`)omgjy2>z^^cFgd;*iH=kaOpBI;)IHS3cY#m9tP8?d=^N zD8l1A#fP&G<*D8ULfhNqtZGAI;S0U549L0B<|BFX!^PjJt1NEr)^=y}J8Aop^@oKz zOt;4p$Rzqi+_5;t33O|i$I0#<$Yq=N?eo_euS@?#;u5z|BC0FwlEsukK=cji{j$QM&eN`vc|=Yv~;>I%)KV8s$wtv zaQTPXNLgvQ<&0jTbGEKnVZKh)r7nu-b8=!Tk=Qq;X#(U1HezE-cdeym@beA3UoOb- zT_W79<ocj`yCLzKrOcpj2qxuMGTX@#!&Bx9{JYG}mU0Sc(ndye4LUvN z(T-<3CGw5iqWC$qbafvRB(;)t9*Pn66F%&p8dfvI_)wbA@_@nw! ze%V(7&otY4`Ht=gw8-gH1~)ls#&V}T@~csa?1?^D_ehest|CD7?C8T)-_dIeqsT}~ zC-yK#R&uf;pvw6s)@Es6xk0F`9@{T$q@<|W_2rw3HZ!yG<@GMZ4mwY|z>;ac^w>PJ z(Pf|vIe-3RSXfgH86xS_ZvnPuX&$!HX}@GcD2Qg5;WN%{XQ$U1J_upmnQAZOcS@6? zT`W!SsvI05NBOjn;}#wkSyFSe;j=`OByuDJ-R{Ss1LR9wPskrQE!A@?MA^)D^+fO? zeHV;+xnyh1ofz_yB%eIu>dHyxa*BWHQ6`NR80zrQ*kXyTH~ZKXtu*P9*kf)498D}V-zX_I1d1aw}K>h>1tgu6R@3`++3%Pd{1_#Ejo2vm@HXBeJ^&v%P6ui zVk?;!4rTS0FZn@mxhVOe3Z;VZ9+TD1>S`ZEyueKcUXD*6K79Mv;%-}8G#1%-k)M`y zElQ3}D&6pDOc1jyJdPF;P*Te4E3&pw8fbUe@d0+7(%6Z{p=f9$>Ha#k+(jVF(+!Wb z3>f7|Ir-tJYuBC=7w7*R?zCU`v3aQ-^|c9XGU@i=ui(pgG38QXLR*OFomf< zy&UXsrO_H;mi+#_^KxNEMzOcp;Oc7RF240&JvSH?#4{k)x+6b0-_4d>ZrE+LRGlll z>l3l1?${n7-%|hXt>?hrkY)I8aT{l0o`4>@HKFUryZ>wh#YXSr=NtV}h|@{g-1Fbu z@K{TqwT{NUjxbD25Nl>;3ISCNQ5s#-u*Xb9K_ktfoKE|5fv962sU&`}cXv%FHI+#z z=S4$Bbnz;|l`3r7=>!{L2XV#fnRm9cZRt|6Z^C&54jNg#B3{ItXA2H(H2@i85P5TM z33LTEs`z_oSAd6S0h+wsh!hoPrl~0&iKKjUo^(OfiR3&BOBfT`d5R0l0^9>$Wvx~N zR$2=^PV^;>fy_7szE+8e@}!XtKHS*f>aL9Wlh4!5T}|zdU%mS#G9HHqI|?^fR+w0r z$7$gENLlRmosgC{Cx1C5)s&0DHwwzlAI+Mu6T0nfbi@yHah=y|c>z^74|WM`_4i5V z=@8dn{ni;llR%LDrb*-PXYx!{4-YrOL)i_C%|_cnPi>r^)VC-3*E0*DjqJ$&^e^D`8GCc`FeYJo$=A|#8Rd=#*AUiH5H^-i9^3G z29`7S@13mHY*C~7BqZKWJv9(wwnD!3Q0U8}SUP(Z;jtq+g!VtWHpoeJ=<5a3pQ9Z0 zz2}z_KMn#F2 z{QT?Q7m|u?@qzj<&SX#?5hQjU+gWhNT48!@R zHXuN7u+Xpnt>19Aar~d?k#m?weNR=KkdZ#8^DGP(XK;3M^Q%n_=Oo+7_s}ohFN~W!{6y`?W8+v;8MF5qODH>AB1n#EidVO6`|3Q(*iz;k zq!A0;9nXRBY9 zW^w=&hu6j_JZ*6tv7h;a`xw%WOSkB=`1hj*prCk;% z-0=Q$##{RftYV?truXh$@uLF<+s^*IfU2q=9{9fAq&w41!4m}*{C9gzo30jdITlQK zlV0_Yh1$Jj^>U)kjHV&mzDzXR<<}ORKwbBIvTk~4hz(dsKLgah>fQBCfvOTaf;0Hb z!nTRG^(85ft8g9l!;L(L6&8msu^e_)$JFRjrzz*WFkI}baHKFZ8(o{16L1#yIJZ>b z959cP1dF4vu(OsMw1s7T4NXoQAx;AmUAJzDD=FuXCC)jXRUX{7uyEn63Se~DBVb#N zwoW&wb8?^2)zcHhZ{A{69Z;R)nzd`FXpH_hru~rwmZ48G4LqDU98dgHC#`wpbKU(7yUVYD%2ryyNu++tN9k(uLenwaOMvN5fs z8Dp6>_;tuG|7~om{yJoIw249AD{m(xQo=py*b;`7QGR@ZvrM;{h=rvz`9fX&yF0d~ z2W`Yv#7gaqVW?qeo|w+Q}JJh4rMW$S&}3YWkTIZLL`iS?L#K|wqaLWO#<_9euqb0BhNuS?RB_JZRfj2;EovnNSaY2q+mawHA8dQ^v#x!!bTxow%nHN7w+;m|Dk_U&&|^r5);#g z-sb4F3yO3OF*6yiX=+MM9bX}0R=icHU8~2$6e98Y20PCIJ7-$dkDSYWp7>sK`E zTfJERwePxTn&oVpFKd~;nd$MQWvlf-mNu6yQG_{tdUfqVaUoadM4tXy7dV)*e|e^T zXIFysLp+#pw+%Y_DxweKMT-k9vIq!Bi5$k|!qqwdk1;MEB{?5il~6vg1hdC{M*Om^ zB}fwN7!?hsfa43}p#o057P4AzR+ZgkFVU5_I1F%gZLJ)G)fE@dSN9n7F3<5flr=Z< zkP^*sf-v;f;c%J5P|S5oFz^BThGdlF)6XXb6M7+rMb`X77Ue?)MTFoxd47K>oqzUT%)PN)777nx||CGc7YR?5%Jm3TeO_ANK~r6AKI zKRr(p@~}}EZE4wW2q;xSg8g)eVIUB5Y#rg0*IZ<+K2nscK2y4x|NgxnTt)?DE!MBS zdrBO3{_1V5e|Lk^<uhPN=TFrZ@ z;VxcsO!k=8E$wBZ1_tade)Ha#c=IyXA;GvAKx#I$`>v!UTP2b^phrq;AxvO}!?8dc zO1F!wvikE^9KyZMDnFY%2Z{%dWbYS2oOwmzz7%^ctqCoOl9Y{wgrhR+i3x7QVO^BX zgNF5|cKBMk$%Ng$oBIWVdp5_+@|S0cySRjrszo+SFD4$o-DU?~ z?Nt|X$V>?x7W8qe#5OSBd7%o#>jj}h8M<@En*|gUq7=M$lvEs#$G&Sf_1{`J8`_tD zZ?USXraIz<+)S^Sms&yBU}}Erb~s9TVqExBd77h{oCzlv5c~X>(aflpyIaKFaLdh6 zo#qf$6%~6KYQgUILd!w8z*@aqBavBOj3Pmdy7bXqGQhujnxijmvprW;r4iEP>y^f4 z+~+#0p8<;|i_9_FVdsPhPem`qlUS1aKlSV3K6mf=+gWDreed3Rd-1KMv~WISH5@s2 zr);IOrMtHF3|Isff`8gTgv*NuNF4nMX`0WE*k_E(}1H8epsw3kxIJGsxgjfxU6BZDAkdX-r@d?*FR8 z7OqEn;cFSxutD`hPL_cv&bp;l`gLQiQS9F*GGn9v(I7Tu__je;tzMrjO8aA+*xv%> z;&YPz31sUX;ywZW^HB_fUktqtY%V_d*xI`w1%@{s)t?%0>@kkl%Lpg7A&U~5=c!F^ zxE~v&U0Irpm_901x9?3v|7lyFP^RjYJHE*E)^I`mB|=58q5L}rlG|lUNbl@A=3~M7 z-8^YSX5?hfYGkZAr;%BjQnt5zYH!=jVQiFdlWTEUWJ-U#zP&vHHEqy<1ek^ZT*5(u zM+&*|3nG6SISkL#B?bmlk!}>%*Ow_eTLRkKtnwHe0}r}ZmNxc1RL(yDcGD!EfikCJ zK$_gYe;;5*q=x@~ixSnT*DI#`_U4J4o!T#R*mT+1H5p%+uWuI1P?Qrqc+e%zdfZHx z5|!i=W5~eH&JF=KW@c}1HiRVBi6vtO$PVkS`;x)bVrSb``+c&Ko9+qO7x}rJwg%-K zls9)we|$sUp;WlP)R!f+E98WQg+(Z99ERrl_=?-dzg@i32d53CBqcwDg^@Xp=O70= zOBQw#Y-YE1w1P4UvP@(Q3phhs0-u{FZbwPo#(vdzkN=+9=m(+I$6`uH0bY-d7*koecp+WDP!xHVFK|M8-GknJt%Jqx9*j;z~2qqQE355|dNW%yN0|P&N`0M=n^Mr)=xw2|o z%U?KVo7N)b?VAs394ZVW-Iw(%r-#RwBQ22K6r5$#rOw%{KH-(?@4iIQi4=^!i{bX% zC%jF_OL&h^1cl4*KA{w?&j(6FKLUR`2YY+dv`n~*fQ?Bq{mH8*{3c&Zgq@Z*wx8K_ z^;t|2oW^iJMMO$S#V&&O>KgVp{S#}M@;ylUBY@d7H39SP&n^?wboAqr$AQ@8)v)~M z3LaM%ld=fjpq<8aKXV$(9s4w{CmkZz?N6yrI8(dFQFfEz`am9~^RrrSAdhuNLk;Ht?`?!S-%*s{_4Tm#DjcW+j$cfn$s4A-emerS zyG>6Ud`LL35!e{mnAlk9FJrDmOGdliJ%3t*{lxmnO{CB-mnJhRq{B8oynkPE-tp8i z;Vm75Z^|P;J~6l(_MCZ`!tWy* zf%r0s_)pc2c*x2*7f~KyWT!~%fI#&&{?kRcaXNZCw!_5>VrK|hoIkY}Lgp{Nt-&v& z#{cyvbK2Fath=3kz^vGP?@vMhMo6eLy?fi1iJ9&hJ_3* z?!Jmz=H9ph>3{ucNZ`C>X73r)f^eT)kZX^)5+4Gr1-=z7cgUw@iRdE z{2GWF`D^P^`}w{CC2k3WptM&<=!p2jq6Rkm0YjUd?MK#opA5+j=r>Xk?=QjwW1nbC zxz8Hq9xBU;&d@J?wa!$B&L?y2Yan2K?E|#kQ=%!>aq&&r=r`mIB{bgu@QmK4jS^IO z+WSRASGT9cZi7heh358RKk(h_B^utm5r-Q_Q~bC!y90|`ZbeP2%b6J6b)JrKBma4G zX?VkM%E-lHlDHyA;xeBxHuw2;NP6?Bp!j77ERb7&~wzREXRcoN0K$7wF_ zequKd`EYDRW9uKb!{F;d1u5HghC4-ApebY+U{p-<&ACE zf6bOlTo8*X?xz|*qd0deJjIsvCYVo8WU7iS%??SLw!YX_r(-6wsK3e3-hC2XR6>N1 z#Si*cP*mi6xK;}~=k)2*R^4Fu3j_A75DT-63NMT%4jEEU$;`|wCN3VqVbt>_qpZQ3 z?lDKio!Eq(GbT^St3v_rU0^X$a18xF^!cG+$6=#$NgyRtVd*m(~{QP@C#{z3K27rE)mNIzAlFoll6Y^IaK3M0M^e{ zI&96$IfvNvH1MwVsyUbdV%CLUV=qlPe<0T=af6hx%9$u?x@w+o;KDww-@?vp`*mJk zURG9L8~ypNWCB|8$(mq?x!~#X@#l%?){r%hbK!TdgmecWd+Fn9-V=YvD=IDq{N8AR z#dYXebk7AMP(uZOS@Iu>Wf43rFu%$f;qN~Qu(DPno}0Q(Suz)t|?&FJ9*|1H%pG(iCtH)7rw zt4UI^fa-maVWj(VjMZg~J*&6)!?u^|i{?pR=_9{SpKdv9FCHHqY_QoKvWNGwT+Nb+ zx%dW``1#Skbx{X^{li$Qg*?uQJ$W)|x{j|TL1lE2-wWf+ImwH{v`xc@v$4MN=_>V< z7r*~n(xjM}7~m?o`3lUOPC$w;7>#<>KAAvLZZ-rk0$kpu`M}ToIJ=d<*!gIGFy9P^ zwr_B-wzk&tsH>|BsPNDFKL5NiX6700x+hrj_`t?rS$H0{zT=!1pn7 zX^lhpAmv(#7StbLv)&!L$a>?3`Q~hUSG0=}G)u}e?!V{Mp!@RW%Lc|vaf@!nqNb)S zNS7?sn)zKVQ6hOx46I|lTz=nGyv=~NhElq+ni#P;apAK4)z(OXa=q3FsEtU#X(Up2 ztSI-2nW2WNxhEnHI%6E!BcqB>pGv0h?##J7nc(KmP`#&>c%N~o)FEi0GaxTw*Ihxc zry&wC15oYh5j;^55&h9JzO*a6DFg?qt|0_pVhi%m7COty0t%bp^d%xqJQDUFKc(%V z98YiJxzgvGt-d}!6#R}azR0Hm(Cq>VNdunr3yaP>P+l=p<34|M3-__`biLc+pjT+X=OzX*(wW4vd24&w=J>8(_i%W_d`3ft~roj5fPEmGN)vT z>+EXjxmR5LR%ZW@p@?AmJpQdlhLMm7v-P9r!7MF^C`!L?{(*tgA6F~RP~sBbdQGaS z_Q_uDxY9r?!}jVl%alvGF1GJTxr@v4P~m35;aT6vgpC1eDx1em25*N`X+P!O+Q2|Z z2QOAhTKbl5)8&+f*B(`wqoE{u8-RW;`fr&V>qPYP@0ERuO#J!xWQ)WLFR7DP|SxR(o31@%(x`}iq^eD8! zdI!rM5Ll=PM?xp!PPiaEaeOd}v{~%?hzyrI>RUnYB9jr8B3}n=tXL$!>h2>NLh4Oc z787YLt(eeIQh|QJ?2a1nbtX;B&o>-J%|zXOwM&^w6e?iccZ2&3Lj}Fy9+LTKu}zwn z7Y-j;xaPBGlWoyLg%;6pbePpj?$FZC)OeG!M|TiJDV!G&_uAN~7JrXAEL3@^R90$l z5MYAoP3?mTR?pbxQXK)B8Cm>9TwHu(`e<#A5X@I&kju33ViFPv)))d$cmZFW?{IdOP+7*+N$N*F4i zG5$)J+j{<)fx%uR^3a1dqbZ?ByT?)viGaWAN|9nye8jpnTUe6x*_M?Uh%l(YL&5VN_ zgU$ijsB_4TtI$OakM#EBu_1PI;;}mpQGrmC&f^zrliXTHo~q1vg}C!2U8KjY*u7(P zW8=7peG4q;yPGKNP0|f@Rj^|+TDNr?{5|j~pA+CZ?vd`=6QvY9^HgIij3q1Lm+ZF$ z&0A5545_iUQY(I41@Mcz$iEj3+8H^3PLNhnY4V-*5RKS;a+Ou@nq2o2&2ofbQvkJq z`fkQufiw6B@bWZppS|eW-ySq`u(QjG9miEA!qvmpGv zouOeuS*HK~Q_zuv^oK2Flwtk#gn^H!AotLzF+e#5r-lCXGZ5DV)IMg8c#xk)NZvT% zKK;mRjV3T_MVZZnxF<$H=lEf4urfKPuB}`XA901}#AB{xi(7XG>TytGkUlwv$?-6} zAR=R3)WES%yzvV7GUs-}>%eHHL)pN;kUx-&KnOm@!FCGexui3l3G_sSSSKF)K}O2H zB>n>n<;HzH;f^6%y3O*0C6t>{Z@h5gjo%(2nfW&hrBDOBc5;9&zpP?Zz7qpMhkx=h zugiQpKNG$PLA#URQX3C+Wm{qXxmRaS4sc3;88e3*KtP zLAYKx;YJ|tNkuw4nWRD6YR@DdwA$0PunQ-Go`}DSN)J}w8E>&rt#BTOO=lYiaM9wI5<H>O(4=0RyKsf);{Wr2_z;6c(UsjU z+p`W9yceaqcCxL`AC~|;{rSnEl(0<@Etz%&vEZoS=r1+2AP|z6Cq+_0UfO*QOMNI@k=S27kak(&GX6Lq@ z(~e)pIO{69tJVntx)NQvnF@|Atn!!AqvNCHVQ+O&ES^NCy0@fDs_n5%HZo}c64a#V zj>;J2;(M)S0<=*}S5^cBqS3@knKO@m)V&j3S>|b8W=GJyly6`SKk7k{b_d|9|^94?9@e0_E>}X>UNS=v8y&h`Yx<3{gU!Z4W}7M(9A+njwupW0ojUFP_pK-SslVNI+s_-n>w2v^4wQjnCgxuM_`ihrj~ z`+m#P^?`cMkdL_#xS!*;Tl9;E;1YzGk`fSv=zks~R|a}}|BG!s997%RLzA%sT$~|N zd9%st9r5d%zG@`i5YohpC;Zn5lB8~zDO)p`{Xjtw7j6C|Kq>`1l#br!&>4$YvQ{WiUgXCA(4RmC51D>TuQQnGOK@icN-WpZgv)d&)WE>S zamanu!a7}onSW%oxw*gN>*-;qsGC*l-mOlX!1FMOl$ zp@f^uORL$$$Z2Ke{F`iS6A!Cef(LvBS{C}&h?guo)qrZb8Hd%%X}z9ywvb8jR&d$I z(2INbeyoo9Y??UEC(~FBR8%Z%`Ysr+8Bw^HkLG!YmX?0C|El>;FOGhzBzkI={D~nEoo>T zccgo!>v}$HDe0*3(A=N$HW!WM-xH|JtoySU_mKSgA8m+|fV~G!C)06il-PrXd zBXt@R%stfX>zyHKiBCxpyX8xXtnSYBJ+=s6nof_TL-5Kw&VYxoumBCq*IvIqRYHCG z3ei78e7Qi~KO@ALgS8~tcXzf%^OS9nAw&xENvELBHE zm54Sl$JYn;`xaxWR{S_)+V8?K+3M`Z-#rB6wB_`Zs1*#Cx1Nm7 z2@#eFK7EBZy?BqTMC~7Ty$udlubm!YM0k zGFmnay%(-sy9U*oc+f1000pVSl|VtN?t&W|71AY@C~mUUzkw_$vuPI8!R<}F^{Qjv z-(N^9G^A63;l(WDMUBJSAp=$xCI$vomv63WvRqWuHfktOCcqTLLXkQ&_e2f5+q)o!y+O-ku+7Et(BhmzZ?J z#>R%a!21h*0|THw-6C1zHnJ{jEd&JyLNqc5tYt_&5YRlStFMo~%fnL)-L_3%dU%}a zjV`uY9tvK-@b8WCVPN53nID*0$Pkz?1xKiJ6v=a(6gfKTyxdYhHzmPO%w--Wq*#rg zA{AM-E}Tm=^AnzV0)fimoG!5_8UKnKe~wFUdS*X~)rQ0>2w)+!WbOSI2wnj$sbSTk z%);8g^`33fV?cYM8qW*+kUbI+(ZD#3m`5Ri=S140-hkM}s0!mz@~gzQOS)yfSQ`fy zbZz2^M`1tk3;`byWcuE}f1eCp?ZfVT4E{Dm=n!SMF^vaV=)y{HYd;rTR#AwG7&}2v zaNtrPH&IIC3G*)`f+OOU7r+q1J^jX)M3mwdow zrsZPO=-4n|n&C)-N7{D$e&zFJvm!H(n_*kIHTpkKu*X*Wz(W_k@DS0v=Gno5UR`}yu9z4EVlF# zfU^q$59WqJevWda$y?^#)Obg+-A00hz%cX?PlSyB(F8XN_w@i!z{QWH*^@8WBIAF)r~dnU(8P*} zD3LE3@}#m3VERze&=eIG%5keNx=pu)H==N3U%s@EJX*|pUDX5quu<`xaA}|^_&cDp z2BpV}rMFo%+a z0M_mN=zJFsg-%Yf_Af4W0047tEnINDpqaqP>F?{4+MmY6d_M6j#8KjcSnZQC_t@LnlnG zvyQ?Z#Q-ci+-DQV@GLm{k4G|qcubn3+6TSvl(qjmP6)tx#!Q~7xgYhj{6&?pKt0ms z*zWP;1_&;MhllGmeXgLu{D3u2M_`6p&j)T;@00KTLLJkYmqT^EkqOM&dcjxV_zov$ z5FhRpf-WhV5debGd7=|smU;CHeOuv-cA1WAm#S24yj)pqk39tI_P~CvbyrvbywxA^ zj{$yRIjE)8A?a#zkG~=L4ILEz7#hh|g|>QR1NMT>E#C?YR~BEI5tU(oP!o@hkbe^5fzvanoORxs&ekWQoT z=GJIfJ%Eu)uN_Ds9$(W$$LN05^%E)l(Y)L+R{o=*SaJ`V%p2~8s-I`&;|({r`MZWc z@z!0>%7G&gO7)Em3=H)2@+~GFmdVr8(S7E3vIosPm}4MBUi9b{J1x+Xh6*a4?JacN zdp-m5#W+s}lr~Tq&SB6|RZ-zqQ;4G?)NhZeetZ5pSP}&mS(X?{xBs`2 zkHs!r{&4Eoq~mE0a?Qu?5fKIP*e+91Q*&{1uje)#?rj+GCZl_^{>)6IMBBgNX72-b z0)Xbvn?WlPP|;Htw>luz1h%-icndW4JlI;OWsY|d>-djrfs}}c)74OjTxa~J2z61p zp*^~iiz<*(HTH`}VCpR#--Fsv$sLULN9p(Z`U=_!-lm zkN5W{TxNlS!RhH~6?J6D`=+KRMn*>ItTv_yNE7_My2+2wF+K_YUR@GS)3C_M)~)34 zy$nrvhU3I-Y@p``o^_G!3dMyB9rVnhgy9hj2XTZUp53 zxknQd6Hb$Xs$K@r+Ym4{l5yicZ0&_AD0ZmSgDg^4IV|QDt4hH)@{}B(3(A&|Zcr_@ zG3iSB2um%e|7*sqGrA2@iS@pnQxBb-l(|<-yEyx7s`}Twl2VJx`YEmb>3`~wKi3$} zy1wW+KJvDK08cu5H**t-&SQSOeMws9CBr3L(q=-XhA~y&g{10Hlx95X*Y<^K;2`HNH$&!d=+*FtlET?eel~Bsb+J+21;v@~wxlR+fOaBiHs#VI4s{gVhx4@# zk@GH-xe_eR2WyazhpN8=2<&7&=i8|muRKF+#kReWk;`VxE~CF@q#)odFs~p1itzD? zfW_z3j+d2P*QI=o5}jST#Diwc#QDe+KPd*D}jN(R<$G z>GEH%)T@=)$THttTLber>eLAsR&xFOP8H<^C&$$phfusbL$>1+19KvT3Os8`RqHPH zZOgRk!@XPT!C9QUYhKwdeMk34D}@tt{fybKo5i;~%eRbVxV!~J=IRL_Qbuz3#zx>y zg(XsrWD}^m$vqx8eI4J9xc(rRYq3N?byth8l)c3%vw&*HSK`fNTx9v<2?Yh0?==1T zH6zmJ?D&qO%jQH;8vE3r>#32`(9j{SUc$ao3t_pwLE~=_|2rzvl#s79u?Y!;(A}{F z{<)3m!~y+>L+475BWxM77 z3HoNih84Lm73Cy-4yW~b%sSS(e)HA@J3rn*7%`6=PxnK%9vj-WS1bV|1k<~VkT;7W zJh7q{b)^C85TdTd6x%Ra7EyN@&7_^t#pbC;#JfLeYF#(*QZDpXyOY(ZpieubU(@^C zl(N6`xoiH?AV0-msWP|yj>G6ixhP-0$;Q?epYvhC!&uD;43s(jM_f3{5WhvOkk@6y zEn;O)xiI3uyncyK*fV>h?@{iLSswNMF57~y#)UQm-nvhiSC+G`9B7VAXY8krE($qs zZSI@rDb!vak>FV&=G%BZS~fZP?DicftUnP~H1xNbzt{Ah<8Esi`TkInm%2$*C}f@& z;|8o)_e_B_y}glEtI7Z6nO#--31fU0oXu?K35)Z&ZATX8rzR$rmP&zT2a@LYPNgj_ zeR>W`+~kYMIp&PaPlxwz-$z6Ro(LVLNXgqE?>Cu67M@Wo&n-Xr%;@FPQj(%M(-i=< zBClV+c8X+KH~o>B{{Z=AG`-RznpCE-*kN&3I9uNm7H!|LG=en-l4o}x_$MaP?g%;c zrdO#AUUEG2o^wvalzygLJ^Bos5<|Qbt7xthAHU=KKv?4F_G&OE{-z_V%^a&@uW6Rw zyb7qi=g*%%dGchom8V3VOf2JiIh*@E-pOMl`YA^B9yY2+a~|22mrj?j zr`sNl_zp%%I64p0FvajPND4qdZJS6f*K2O$S2^cK;tNC32TClOwz7HLXzBF(-=ZOs zPb`9GfX#g6Uj68)9CEVlj@>?;a$RIZRm%rC@B$vjzI*>37Y}bZ*Kx_`s-I0Rg$OGh zdw0Ix$m(A2%!rZ8BmT7yx89eT{ySEZDmT3ODxLHX3VoP#)& z&(8XyyXKU9{8k4zPn3rW1s?Iy_H*l%kuQ1hMv=a@6Y?)0GODRZCj@H%9Dn!j-2?ok zq~u@my}qL(9lp85;Vmb`!ZY08MLDM z7KE$e$#1RyXqu{f*LcJf=8G4W(w~=cht@Yd`hp65e7in`W}rO<1qsPKG#WS9F`by| zTyct=6YZ1IzJDN6{K(-kkLZDDu}b{4$Fs}FZ)3&%Uh&|T=Cu@elyBi80#4xF=AX95 zroP_dKPdL?-*H({Ins4ddrrb50%i@QYeht&jCrnId)cvky@pYL^8E*T(f)sZ zsPvj=kn{2UobB|ka-mv8dEu3%^A490xNrUpmv;r{$Phu*UtNB@Wz}ap2pwg6bo+iu z0NapmtVf?6t7f6PUdrK8$r7hcxa!!E0ennr=fSw2AJ&B3a@{+6#VVsMhcBy@bUrxJa?- zd9bvxvg+oH;QjOIZ{~LO2LAPXigzNBttn*9s@pd{f6`8mj3>ME-RE8!y@&%Lhk=ep zF(*evG)&y@1hVq-HJ&x`E?UMzCRHunv_=WLZxooFmbydts5Y%vG-Zb>f&_RwT%>gu z@wP9!zB_R&HyaB+y_haNeJr02iC7lL51=JvF0yHhl^lq}hN&wW%&m~_VCLAAu1@WM zT>v}gcLG!V_K|?g7Jx7@2F{_7GItoky-Z+UHuCkdybUs^u)Y!>d8A?Sp}CD})Equ< zaALo6bZSPTuo(dT2`83%Z`atU?dsahH?Wc#eC`bwxm?pl3XJga?`{k!)5Em2J^nm@ zXCapw0q+(JZtX=~l54Pp>=EKxDk%_n^_WzOf-GNxQ-~Sp2p^V6^ z2-%Wkr$J<9C2mB>h`Q~)l0r6RE1QtLclO>pviIKecW#fqkLUCIx7^qJ8s|FKIj{3N z=fLayNeB}ohZ>2JLU_UP&yi|Kk14Hi&|=HKkhg`9)0q^6@d9rx)j9SXr>(HyC{R`v zhdYNrVTB_Lgt32Ko6B9k8>bn2bcI@9h;cPq8Nffjw*#{ME!S7>t7XoCa!6MI2Q zq&63$+$+>5BB>}_yqq3bk{^;Py?1sO)5&0l`f)$NJOnzBcyaHZ@K<<-7Vf=3bMm@Xdl|PZdiO8wNh? zdjEGfIkE<(O$dWVj%3Qdo;K062Vokbh`2=udx=?JhE3A@O}`qS6CQ<=@L{{JZ|m+0 zJ})|=Uec-lt>JyTat zz=&czeI!?}GKQgO=f8}`i?a#0VbXQ!jV5ge5KWV*_EyPPj1~eyqNG3OVD%kuUR-_ zq}bof&&jK>ggZJl1QCCEB)x;WR89H86)+Ae>4S3`#&JF#2Tb*FYt33uNVm5kC9a`5 zmp}>6m8&UL-Nu{(J13{X@1h+Xwjy@mIPPsN9oZG!x%h*S@n#oqm*NbTJg;y(;}FxI zy&tbTsv#X?n7X(bXpew|gqG$9aWkqs;x0u=z zzx2Np;qAI-iO<5L+)(q6rI$gGgss!YedIvK{r9~xFwuzc0hLNJ&p-3<8Y-D1R$qCA z8^3gS<&QnJu3Fd68Z`Z3@Yh&a7?^tkOMr^#FqrpFwKOV91B&7Ry!fUg4%cj&4i{Qb zR#jIc34yhm*F4dHLTyyl(xadVP_EHX$Q`}Wk&*jjgXa&OOjE_6lqjGiI(mA2-yeHi zrV)X9CGWEE=u4Y$XlFH})|RE#o<0MyRJWDWSW z)PPG37MK})VMPFpB0&!sstWdGEZPPJ(7P-7mRww`-tw4gd{&m}e>ld(-v?zQBJ?Ic z2(TScRjJ|_3em36JHYKhaeEMfY}_)p043Ha?M(W?e8|TJ+x(t-QzlXXTZW$Ds9CZ= zDK*&(aKI~FRH~8ggaA*)U;{#nbA>rdxc>0gQam2+U4w3Bh}rJSBW2QM)zTqo3Sbm2 z@kKx+7kkpEbBR#IV_E_0^$Ym;Ee#C~wY8A-J^SkuVg3p*x7zn%=iaVmOMhhsfT_&! z3`7#7#l&`691bgS-32bCu)nIk6e6JuAs?hQ_Fo$8=F)IvWVHb6w`SPU?RTkTi;Ii% z^R~b|USBs&;s?@sokXiplh2ew;{2T0b#t1rlEdf_bQyHi9cZ4wScgI-IGayDGnfd> z@=wStVfB5o=+#0qn=Zo{g9Hq=9`Ri<;8OwdEw3ach2^Nb1CDrMem;Sal=PnU`s>*r zs`>5p_4)wzc2aZc%ge~fSXDX8H@>x~Yii=+=APO;Cc63t2SYESrVUqPhc?>G=B14)&Y1rgfFoI2@uoAV|i|4O_DUer8U*5Oc7q z+LK-A&mP+a0c&nK8snr*e8`>XA07?`fFFiKF%-A=@1XUp_nY_~;tal9$54W}X^8I429UkY?G9m+${#2~$%Yma3{0 zJ*CJA_f0H+2iXgkQ6@*3RmLe199IYbo{3&HI|VwS%ZhyI|lF99k3iC>NTuq)2C9vYWd>L*s@= zS^pVIm9C)sMd*FtPhHTrhhF;lAJ~2NNZQ%Nz z9l#gE{M?7LXJT*bTZgpi2?CY&^0c$Js4BVe>3xPl0`*5Effii5Ezllg|454(CAsGoo^R-l4hr`K~;z|$Sxwec-n z8#u(-%bW&?{aMO+2GZ!!dH#Y|v5}>4em?)<=^@4N1i_%@;+Z}V-&Yt_G%5sV1%8qU z1%13rIl?K?|NDcCypld4fVZecs=!pAIs68N?m-l=(| z6_Dgs(EKubWj;y?S`quETNp3LD-2l3)3M9QPAe$HBQtf{6Hn42DS4h-x<_g|Nlg0L zZ7P+*cITlvk*AXKJF&NJ-z@nkU!8Vll)`T@-TJ)$y6>AHHF4b7T2r~@I50&~*>cXZ zPoFQD=#Lwk6Ga)FwvrXNAxBTTlyj;#N)P3laiDQA)in)wjGMuC$lh4rH&EJtd%N~@ z?qM3Q4B%hLUXpkbTQN%TPIedG^q1ZvKN8k>!P?w+iBk&~7UK4wg}jmH3%9iCzLnbt zaswZUzBH(J?{^40SAOoZ{Ya|!+`Ymgr_a1A2|#(mhdN=0JxUDU%RW*fx3^q4zx2tn zo5v68s2{a?Q=%s@{pS}{jNiTR5BW9M2Q&Ph!K^Ox@R)Xo-`{_%=kJePR_%Q{)+g&& z;C#yYQ)0Qr#QJTV;4#rxXF!z*bI<>b)>yBve_$xe@9C~pbLPM3aQ3$!S?ho+(EpBb zrJgz`_jbmCTF#^|G_(H1^9=XHPy46IcOTgojV7!G0746=o_?vM z&H7Es7GSb#+)LCRUKYauP0|Eea5uHI$I@~~3ZTjCo!&b?)yBqbw_V%!-sxw@g7R)& z6&)T(Z10k5FS1(b6EP-#W$KMTfWe@I4Xh0mhnN z4&iop-u};2R;C|3xNp$&&kYk{ne|Vz;GQM#HUBAHth9M|rnGP+8AVO?FY6WHx6mFw zDz5zBA@o_d%=N+ipkKh%#YZD8+z-IC2`Q5Layb<}%g!bIOx$(QH^=hk*nb)&QNj%z zpL8p}CuqNc2WP4GF=BV&cl@qWr)+Ur>gp?e7rl>&l8yaw<6hbS`Qhh)z;KY|V{LWr zt0#O=l`^|fwzU21e$ySoKoicY;reU{j=<>~oId@ePfQ*;6pLFNNKy889FY!Fh~YoI zEm?dfcp4V188KJpAEDAmcGS?Z4b z!!v_Y^1e|C&-L@GN)yy_6Z98|diuL#*Mr&_UljL$_vlxV*H?O~aLM=Md}!?rk4LmO zg06L6zfJI96A^So9xp)I(n#w+-WY5$3FTs^#!M!88p6hls`ofmymn(m-P%M`K0Ly} zfb7R&eOX2x_Wq?v>ULJg%Nl33kf_?rKZ8hA*($>$a+vj-rL%CJ$vi`TZ^2XxuO6AO zWso;x}I9)}ac$SH4htTYS;i{IFmEhlVq4D3<9y+4spGZ__E{RV@{5 zo-x!Ut&re&>_UeTXu9N6e9%%=C2}+`{Ldq!j(TYe~FlS9w73G*cFL$owIn2%O-_MrYtppW%Mbs5^)e{3PjuJ)HC}u`R zMy94n$jOrxlbQAch-?~dDePR$&zyHXy)leLE0IglQ>oDiAJ2*`iMyU-+`%n1>v}D< zI5^R6_)EFw3ujB#28@R-W?vz4jVL##7Keo1N?v*TBFC`Wc&!{WI^T74^y?KCWgRPx*i_-bmP#SNqo++G#c^1-ydH4r6%bdFA zy?Q0DR2mJfj&o;bstjc6l633Ruhmd9-SIAC%Us@^O=bfi!TSx+O!+tGChoS%HwUR+ zU0tis`d+F&A(OdOZnp{Rp|=gq&3C)IS}%hWdW z_wyJ)E9D6fx>BG~9?+LbkF--CrljO8vRoZkGw+Rx5&&1eeMj!?FJCj0SBsqy&5C!q%@WRuyc?MNGogj0bVY=XgoNCl*n|2U zMo~Qro9ex$>Pe=Yvcn$=$=$z;g)%0HnI^#-y<>FFeEBP>js5WWxT8)}$Z|3xe~C_P zyjyxgB}=A!r7o4Cp+&gRV`$q>C4|?#Xrj=3kP)5U`HLl!#wV0naV6BZ!*XxIVHKA8 zvT|4Cao@@SE$z1X;$V=W+@Bzf!=l?Mv<^0#7PBK3Le#vbwOw6YH*P#gQS2zSBwjfv zJ>qxIds2v2QGUL99Mv@AB@0`g+QbFQ!s+%^fh-(?wUJjB$*b#x@%lT2gM;A68)Sj! zITFOeLT(UuIj50#Fyl*4H-}bzMA*xnKV#nX5+Qfqvuw@vRMzP7<(}cQlx^drt}P!1 zFQ$1#fzjB!5$=frzXCYa$7%uqWRBx zt>+gHccg83j#ki{qCfF`fJ7Cdwb|Ik%>5D0y`5DydL{IL{^pL*Rv7x8KRVSWJ&pzs zPy7a@D(8E7%Y9TOYy|Jx?_$6!+w${$Lkda#nCcx6>O5Odz2cU1aG@(w15$qjt7$0P z`((daSVbvScds&p@oA+r)S+2EeVg50Ze&r~Hq^`Aej z`ey?CedaeSYS%ruXRv*!ZjL9=OE-H_^c_t_mW~*efLtl{sPAO6vbGd08rAwZK+;Cc zpUS;JemiY46&;6FgKYfN@aTFTK@Z*F;P||Wk&)pY#6{b1=uy7buoLzPxu~T6_6E1X z>*7@JaMHdHl)&R!x??ewEEV2AuhcZmto!twgIaLVH6HhsIf1d4wLJakmoCt;)1g|k zTr{vM-)R39Lu>hKO36MW*3O5Pvso4$T^3#k3*@e=aQTsO9^&w7w}L50ZF#?USZ*smnrS?RC2#;BZ`W1ude3fz)l0a zw!J-u_3m9wuk;rmna%tu+s<24_8aK*NPiZej#gV?n%t44(uVq^y2Aq|xo1*>mX~Uq zZI?$J-X-&z8;;Etx;PAJ@u-k(gVK9nFRNsq^EtTG36#I{>Fb;|G&nIjY@s7|Sx-1QDGeX^Mb8u}KO8e@QMXQPS}JAh`Tc>oL5TUXz*d-Rto9h3E&BQS$=w>S zH?M5EQV|}*&xeCH_B>9_HRw3!NYL=^urK4(|^Uyk$Y-Hfnn8|!gUmfN8E>iT~u3D)+(!#WYt;r zKQ@sX%rlimBtr;9`tCRbk$5lsEr;MxJiohVG{RB8^7KTCMts$O4N$}(eE9$Q)4QUG z7ddx(Hs@}nG4f(M!Ym6tvM?2 zl{}PTkl*k)c*EErd!%wp-pl zV}F;Z7STW=lEPy4+j~+3h%+*{|9yd&&GN9r769%4%+jGriREL(|yH@4YE^D#f<_32H9Bb*dbGVyF7TJ)Yu><$PMD)~3?KIW~{WCi1dsPA2}>(Y-mRu^p41t)3UTaq{B0 z7po;sjSjsic5&!zay+CUR(TPk3Y;MeG{@WXX(G4x48CuW)G+Y4^Kj{Xnswxc?+0qq zv$y~K{`3apt(7J2xlQVbEd1Ls^r8sM-3N4N4C)wr4L78`3W|q0_lu1|dXk8EoqP#S zcHwqfPk3dGvX`bFT#~Epfzg_gCD+fRUG#k%5qSHVSG|qTtR_!Z-ZB>K_RadRvM10B z6Lq~FJM|>zMYU#DZU_8aGexiUH}|sMeMx|(ktcd%oT0jO_{VtJ>wfeE0(2>j80d+L zvZTJ3%@H3Z+x2dz>5;s8$|Q=#`RAMr(I$!LdkQnFjgsD@H{TJCjItx=Mf6YhM7&VV zRWI0x1$Cv`h_9*8iUtPhbg-aBY4+V1Psr*u_>G|JV7(v6@a8Oy@QL#gi%7$Hnj_k@ z^T}MeIig8cX+|H7{o>x7);vX(W!8a(&ORl~i$7LUgZ9G7yb|UD;l1g%eYAK$RB(1& zf)hSN*VcumkwAMHDGx`wa@P$uZk343$(QbA1P?`ncaW!0)A9af9xYL5y^mbsy^vd})5!CCnd|y)05@;tRG-yHP z9B*yd9Pw)Nm!4e8ScFfhu8BMc0=K$+vKuJdcZ38y-jv;qwsAHk_V9`R>i=*uQJ@lT zgI@rrlP~khavr4ixI|^Dh137pqi$-?gig&PfBr!B7bRJ!X(FN!7}}Dms$t@>;$mWT zEj5H6{OCZWc0CY{`?zrR#ju0Ro?JbVjLSXAG2@RSkLW0HqwLsmYGrQ}LX>2!MM(Ob zzdfW}!F%`l<3|lW@f1bfSFf%~iUr-{VLWJ;Oq23Q&5DS_$rsckO1XIuq$)W_AARc4 z&A>W~IHPlhNBk9ys8@_^;*DbKjn(Dl3s_ixqKO0;p19nDm-;7FsFeUbZj2AH2uVMk z_>o_aQ1`-4Ti;KecEKh8z88Vqqetg)#XcyP*yblEGf~Gb2L#;t6cqhkP|d_HnFg-a zhaG7RTKs=DR&%pwF2Y2n&tF!f#3%Y!*6X$#AwCy5p?WohAy;IvmjeCh+K}=UZH}$! z9x(mejrM10>`p|HvsCToP5WZq5tna`UGk>_5#mdjC@nI=!3d=!uUE{Z8f$SHag^AY;Rug4>eVR@?l zoz1p&y2H06-moYHJ72A=CUpEeFZtxdOkZYvbaZq|N{Vd!q>QBIWWx6%XRQ`|#>Uz> zCbWxM)r6#|GejW-U*|T&@jN$YcJ0O9ae#DS@`y`7c3>=49XqFP^CK@mK@JllLVjDP zewqODGZ(c|2}!R)XXulkVw3F$&qm=dIP<_0-{FFZPsXMH=;5=+WOrUejvKU_pCRNC zMJQX041OsEc;jWG*;yKzlXJ~1QoHqXs+d=r@m7Vuth*z{%?x5D#nv!+->u&^Ei2IB z_7nVAfY)wjJGn!Tq>NpM$H^B??pUUk)zU^TM-Km<5-!w;^ND_>C?f++SMQSruFv&n z&p}dx^2drPi6@b@JW1nXzQ=FHiln9t@#!&k5NS1SO8f)fl|6inn8<)_C{$G2ei>Jy z0Mdj}JvCKfjEJW&hC7zkRKueljec}?#~n$HSFlMaeW7XJGq;W@RDGl`U5FBFiP8DK zOo((RDQD72N;$ktMwOmg9uZz7wgabdn@VklP)HPka9AFEK>oi&>GUvhuY7x&`Nwre z*%bXN+c<;2Orks##e`VC)mjsGq|G8v)A=sQ8p-`_ZBY3ESJhdw{1cAszQ3%YXpW5t zhFa52p3$j<8r@N^tuPUgIQC2|Pl^-Frg7dHv2R3wXdtHt)4Ty08?3CXw6wpvx&oVW zl=V@SRr*|^JI5S!7tK3gRT}1vBMSjjP4Y}c9)0xc+S-h8dFQVd*_)56w2ghO1EiGD zj@&~vK_j|H%Ch>UcXieO;_!pAP??5-BTGu+yO^?S3;I$+B^Zqz+Xqw-f`^VP&^q{sFs0F%zX z+>2o;9k32FBAy_oFjzO2m7;=YpEWbMtd`^-o(8gr(0M2 zdtEiGW(qLOTiv+KMq#-`HEdyd;+<0G!*c>9{DnA=tU2!^mkq`bX&h)Kt{n^je& z=#KWMK5$hvjcE=qEI2#FT$sZrI;Qd>l{8VaJiI20w|T}^-}Ixm;^W`@>-YH{pdOQ1 z{+V~3+WnYS_r3h^`h+4FBCt$4XJ#6Vm)4>XXa~!V{|AKTtEYh(WILE&aUJF|NG)CV zo@cfiFFv8qK#%0U5dX+uZ{_h~50mCBeoro68!H1AGQhO=l$re!mvtZp@{p5i6-*%M|VLB_?Vwr zFFWV;7gim&DW-a?x<0cjxZu2s+H_lLX-BPo3bU!@mF2AxnEmOochu52GZobjs=UmF ztzgF=mUz>balG-WFNzr1X+IAee(uzU&uoC^H&3SsIbsh?MVmm)C0H;5ttE@ceW(~t zbmx{{7Jd{C)!V;t-HnbpGm|ekYKLbiP|KVd!&;UIixz(h;r>X|{W}Y1r=2k={({W4 z>(=4ZYs0jqPx{dxKlnTLIL~Z_-@=!kec?j1f{}ma3t5D+&;|HB*>16d$KZ>ayq7n9 zR2}>vwepjk|Cd%)N7hrqMUke2+OQWTZsx*?r~aCe^UzBvUMvgeKNByUQ+TF2e>ghb zXkY=y%7~i&&=}Swx)#~_%ZI&dF4&wIGuUV>r#>*Bs)W>7hvz;PA;W&^?>!xkyjRfY zG%lQy0P`NE*)gtTOP1&&PyPGbsY!Q?{b~p^uEa=V^UTic&ZQZf#URjFGD6F~QlDJd zqEw}K_D-PN^Jl>_KK|Y48$6+lUVn`B1tWe69yUBQkMW5%T6ibGX~4B`_O)-%`D*49 z)je)jOn`8a#tRlLjDog|bOA@~U9mGVg5oRQ^tqdP`FE+EjO&9aZ;dC2UM5)?eZ&5~ zh>l(7#5lf@Hb;E;J2!W;0B!^rPkQJj8;NoU-1!Vu0uidaFG=Pefr)Q~?MLrQq3|`r zY(77CrRj>ybt_4YMuHPdRHSE4OvekP1U)g>6#aOV*oeYv`y(mbVNF?gmYp1#z8%nH=^fac}FPk9vbwcS3Lx!2bQYxTci>H zSHy)Hvb4R;{cGdlY%?O)FbV2p%V`9qO>yH8j=FWP&uXQfSa7!s)IeW7dAlBukO7C@ zU(10**$erjD~(ySi*lr6;o#K~rzwrkxliK>ChCXQRvUuj@w+UV$yn39Xc6I#*O_v-h_+_3Ak&uUel%o~}ncIMpV;MQkQ zY~u`i49#qBlY~Yrz4ZWa$)9iqGEj zE~|B#XSIHx%pxOC4df*vBF7WfWv$UjVK&UiufIvQb$96=zug)>w!#6@fQxapa>Bwp zjDeLQ=3H}UjDNHgo=Vi_#b^W?E*=tq0`3!;W`-whnvKv4qm{KxPNZJ8?ofCCkjI6x zFMlTLW8&Hv_Z#`X11{^ew>2L!*soPX8{@YIJf65L-Z@%n#(a#4y8W^K z(am-*P0T8{iG=H$W*U%O0Vx2cc(mp7EkZ_xZz5K79v_2Uq}|Bk;Mo(aoC)^<1^}M z=S7cNIgS7@D!mXI{rn>@mmUY7M&rJ*JNEbN_)>-d$G~Z$IYX{-P#CF&=n#M!p9&&tz;Rr zJS?+&U=A@)1{;@CV+-zQ$!l!Z?g zE{hkUIrwg$7r{DNxnBvmd)tg62|hABjQY)tBn`q(VfGCisj8~3=u(kc;$axk)znL3>+sChjf#*KPe- zdizoq`Z7ISA0K#84nXZ??4~A&^Oc8~&2e{)oUqu3GeEhWZNH{6hl5Hg)!{=QlKI>xdDVr8qD5 zSWf4bO>1t$#@r$y-9@9eiVSE^H!r%jK3K6dI+-)mrKApVJ}SP>B!#u!#MNYpXR^a7 zrU`vL=1YQ@n3zIBrBDGT`vuFS+_OPFP=|n$JP>gZLme`}bk|P~`B{lt$A@Y6yj}eK z_l1jt;r=GPEG(ww&bKmkr8}+EL$=zBZE74HhvsEIeX{R}E9NwtWiW!!*?#d)p}x8R zhl&tlDx-mDQC*|HGC4+FPPU>qg54Rp2yF@+2iEn zbJk9|OtYJ&%&ERc@hfe+c%b)Q7NT@#q}-*sx%v9_>&#^zcLInk$l%?&sw6Ln7>-9> z6|1w+mrkxqomyI$4Zf<1Co{_AL5sNg1at^l1D+qz+ebF~C^5Shf>kqQ$2n~AF5L=3 zaL5K!3{p9EO=f21B?LrwqV-bSi@rc`uOu>zqV5T;2Ot z&6l$3h3d_>0yBe4hFwY!KzPr8e_nmz%?I2$a$@b8;a3Atkw?*nljG@v#n;c3l?`|2 zhf5=E%+#SIskk^cWtvE3_=iVG6u9bNb$_1;ZTE@!*=UM?7ok#G7Ljvn}r{uhcTyvjx`Mw8va$dGWm4G6~A%j{h8|Q(<7Vp7g)F ztq>ISd1J_k$s-j`^qoMLDj8gvPjnkuaRGP83VZU#{g?a z4-??E3YbOUDR}gaCqNyMxL8+x^XD1~5MIeum9l4`|8tS7(=GO+&6XF*6_Ke)kySf_ z7wa2beB)}p8JpF--epin`=HqRz(wWmazK5b#PE{vFzb)zNDI|c`#*(Ccv?cPpPQ6a zGlgBOL(;{utI$c^sJSyJs!Fq@W1qFH=-&y6zwSywyxko?zeKlimj7lSmM8KAz=^Rc z7I$|?{P-|&Q{X!jTGm%dui1|H!pLE%Ca<(5yu&w-xw6z+HoH~Q-Ze}Vr7q=9G2iBt ze%DZ5Ub=R`Yo;$`wYs$?wOiq-<5raF&0=8uW9`Dc7Z4qe)>_oxa^v#fvo6+P4sT%7>2n|l36Eag7l*p4CzI#rKTc0q4iyyt+Tt@etb9OrL?HMONrWTCR@_NP`$}C=@!p<4 zYU=rR!om>PuL%#cfBEk5nn}yQ({F$pUPVPYg0`f;%=^;ERDzW9OG=MenUd-Dw$h;A z>duJpwLLC8s(m)2z(i~w_q^o=uLgsy4iJa-urh|fcza0C90a3Df(+vkXoyBS(R7IK zX#q2YnT0%acDOX&!vnW8=|lCDUFOBLy`=^&i^0Mz(M7^KJ`1DdoloN&9Ghvy-Pvlz z1 z;r96mtcwW0$@3zzf31rmHHe*rgcKkfD=jqp0 z?{0FL)tHZ|Xg=fr`5IjDOrt?CMFo7KWprR-{q-g8+}DH6&(QKX$&#H|rk$xuyRo5a z;h+l~?Q3NjGQlEx189!GF+HI@w2a}>iE_{fcm9eSKqKI|AN(CsN>a^lgb0WX|MQof zF*hw14qgM`AdC#?=k%ncJ?!~Be~%#`(fxB-21T|hC6Z`qkmPR9T|O|c*6G->Pp~oH zZh$-9K?s$5<@&<8Y5FH#1v!a^qaPoLeWMV1;Y?9gSa3K3Ew^E4eL_-jp#L!^XRTvv zhE``ixkZI1K;3q##rF!QzS*597sDi~m#-4hQ1+@=lO;1++TvepW9&!w={+wJd~z~= z1!~;p_a5670f(U;UYm-&XqjgWtYsqyvLO(J{H8?$d-IEqz5eeMJ2%|Ft`?(^+_M2V zJlNWIT`6K0z$!Wh!T#qjkp>%5o8h$m=nN!CqIyGc?^GZryM6m&tJpI_QtiLr>=V7S(a%rBct$l1%=F_B>A!zD^Sby97{FOL z?)2eUo|-4VaEwW@l*97af)GC-TsNLI%N`ThuvO0fc9n4>n7v2G3``k=xu8M(8&k(oxxb6H2ZFN_kG^zKEo^=j{~! zd}ADpdKRis>S-ZL$(O?enl@K8+a|GJtyY$oa*2m!p#t}Rh9$^XV-`-Q06PxjzE}J2 zIQ{<&lC9sx$m;bcDA}A%VF2Py@eo0}i7C>-$`J90j$Qo2iKeaHC@5|j15ml0^|eOF z?fxW!Z(cM%X2Sx-kVGoIC3bocybs|rxb1KV6~cA%1XaFrFWG?>4CjR+p(`w}OU3*}2ODXII_V$)$hYFLy{xlj=%g9t|;9}x;SXirT@BmiU zi)6(n6nE`PuVe;JZcl_j!;ch|ZXnqV7yHnx)%4_ZBv({SYd1`08T7ahaqX37duv!; z7o8zGJhZT&lxjU$z2b&LzA!cFrK#O;4=LU}4qlV#%hTw+hEuIbYE}+r6vZX)-U9Z_ zkKT&aiAbbfPT2;K+oN8-n&tbFAreBo3$S2y=J-GDGv5u?quRSkPmvqR+J?#SID3F&Dfw;KpUmd9w zot8Maa(u|qj$!pi_~*G#3!1DJ1y8kL?^#&yn%Er zl|#Mz3&o5IApQ`p?V0G>drd+leS?tnX7X<7U=%*e1O8(zP}j)QJ-vWpQMghppSi}z z$45su>A_ZFxl%=UIhk1+%=~+^I8_4;b>few2}As5+JieNR?u{&=^z$6m{EC$6mE*^@I#X-2FM-5HnZ z;vYcX?bvB(%RT7J+(daW-9N;D4hd|acajgABg61_-fi#QEL=9WWo&aeS>6w;(taC- zBsi;rfkezncjUnLZ8J%$dz@rECcGhN=G)cOifKefvBI* ztp<{>Ug6Bs89+n$8E73dh+&xXi$%4-G!a-)vC{a^MW7rK1-T7-e*)?EN6VYCVc%x0 z+fdmol;0|&tHUBvBJ3W=&50IoELNzK25E6~nR`R|Ch?nCoM+CQfkp*6KYvblrwl-B z>uKa~O78)Y;gvoN2m@#5W&fxL(dOJaZ zhuDDcl?DX`jyr$D| zo#f=?l#5ieUSICv9{+LiRS`utazYn4!#ZbZxVilf>VkTH1h=GU((@xcWt5my*U~KGxHCXT-Mw{Xz_HI7!QTR6k4oI!L}xnA(cQfHu5f&O{Q2|eZyWY0XZYz*E%P96m&HKvpehIY zcg2CuK^32U@}$*pcF-@{$~LDDa(TBEUZp`izq;mVr#Tk}fNW5ch7VzZPM%+)v9Z5D z511Xye*?tT-bi?Gn3z2$6}v2VeNDlEs0IdfDDN6IZq1d+X1gz3;9+E%Oj+y`4K^6e zHLzJEsN_mPl}e}&+PKASq>AGA?l*n^o@oe;lqku-QjM3k0OEJZRI~{R!9_;11pSDV zlBV?ocnFr}4Lg#1plZ@ARFdUlf6IOH5YTS!OdaXBX$=bn`B}; z#2SX#ZVlCd%dULkRjEj-SaVYu*gZ2P7M9arj{t-JSFJ=wZwxaxvqd3%7;lZM@Oc=>NG*CjnT2OO0K$~U9Th4 zxaSqdBAc2#J_8YQpu19yYn89oS^{&{dRMN44qKrKW_|1Cylq*i2};jn6Oy_f>v?aJ zv2y9#@NY3m^Dm!h5ipm`^NB68dBq`iTJg{zi93`-3nTZ(Qhv0s3kFnPEIG99JBm#g zLLd;R&MGN4FvpEoady4xuJp!CLAElAEs>bGzxl&qqMq1@hm}=o@4Xc>Ub<>cDMLCu zi6I6Xl#vuZ^Bb!TY;*gaxRotQ4^C|Q@OIY0d%sP{7Mv>r1MYRtiiTcGJr?M^9Ma%O zKas*~3iB@$V)DM4KMoj~cF-Ty><`2jL<8vx19$FEg=YxB?x`DCKwLuvY^k_vTVHk(MsK!I<-)4V;zfn-uW#;Vqz|%!F$KMec{B+ zmdlwf(REwN!ojXV%D)+BApB>a-8Om$A4S_b;E|rQA-(;^%N>!mQ(tf{i-{Ue7b*(_ z&A@tNHXEuJ!@e#XDBs@MUu#hjth%zTlmX=wx)bHx_Tp~k2&f1;9P&Hdx3_2t-FGh0 zvbM27wWNV%qU8VS>-|mUoyKs!p8EPEDCJywuo>F?YZQtyO8tgfksyaqa!a+Nx;jp; zBW{0l*Gn4j$tEK!s}8vZa8F-Wez4nJe`jGawTrr{5U!SC&>{j&55A@9j}NGbQ)t08 z(O}iCqX`VVB3`M+g1{$mD%w0rwE;Yg8f6rBUBPvA0ou?q1yOx3_#AR|{@ZXkj zRmw=#%1$V0OAnMS;0|6?NCfGx0cL*DcnzG3;xl>MtlyEtX&2&9fXGn^C-vj%Hr6M^ zHl&Q;KO@oy`^T(xy7hsWc9CWxfsAas+38$zp~VuE6?K6sma0XT0dpk8;fQQ~H8(uA z7ccIj8rLf-=56lh0sPvR!2_GA-<8z2x$t!7oW4=Cm8i`ob7qba9JAy?=Xj=Sg=RK&%|(!EVg_m^DY<*WwQQ-zk_f2l@)q3JDkf$*-Sfbm)>f}^tf|QDnb!C0d zuDv<`iI=;>LNP&{7#Sd_ZG1H^={rK^XNE-+P&UY5*5H;SmwF>3x6s?CI+s(v58Cz< zKP@BAr2Zgpw}>OdDDy9*YVs^O+~hg-Xi#P*4JdDu22h)M)?KbDQsS$|!zI};aWII2 z91h*t7RJ_a7z1;4)Ysxl3Og*UC&m+?g|bm!#`YpYy-9~>ApY2}?#vG^$$X%YMBqIT zIFS%odPDTGeOl8K0TSuLC=F&&9xfnpaDaS0Zb4YAaqEJ7G46eJGwiT!d?@yAC4CB$ zI8L{X7%lMn6R-NbBu&hGlULr8F^cNV*SVE7QTP#49O$}v>87SU{njWoBO_Y9S-xv3 z46I&l^9Eo|Sxit!W~M3Yw)8_^_B>!0LFM(e&EIh*dkGAM*B>5g>-FVT%gx)H8O1IA zkO@pmw#k8crWE={e{?)!@~nal94%D%klw>73uhEO^xsv~<_NW7Z*$?KFmil*zuEj%?x<#6ZKa;2#KExQcMYfQwCg6WkA83D%^H;d>$lc7ccpEew@Rp zsE|QhGZl~fJswkg6tu(Wsr;~^t&$;QI@$Q`-PrH$LC<6^gZkgpW@~Zr zlY7w^7cI@I-1C){|Ku83WfWg*l!d!BtTrCs`U65pRlj-x{LP@~0{<_3Ak%hYRMY

iD-^JcAMCZ}n^~C32Mgo=8*EM_xA`)UZCYL20??eOqz-TXz?n^8jG50J=s; zM-ZroK##7U&Ycqd#8s}w79)vI69j=}-x=1$8sy@R)3WN{4n=z5N9@J(wVOpoWW0jlVxl9Y04Y*NBbs-p_&xzESa$ zeGi%VA8RGId7-!{r(_@L2DGkY_dBhmlFU*u0m;aHs~3WCZWbgrMD=hSmImHOpFycP zRhf?z+NkWqr>h~BH~8|E$?I*qPxdTsPMA)c)a$1p=f|Nb~_@C7m?pUAB^s!XpGq<&JA3t8mej(|W*!DJ_ww$8sP*Y9W!qHr~ z+r4GRgUVv;qQD58 zn#kVMBLPN=IJQr;=MC_0@gJW5+Uj_rs8FK0DB^x$H3`SW9-ny$YD+678R!I9ShhR3 z=D$Wz)+FE^D(;UjEtNg~_C6_w(CuJ&*AbV~&Fjd#w;&W>7W!j`u+3_np`)R3@sf@H z(=6%CGuxgq1j$uE-}njl%#`8y%6Q&btt8IfvT0|1;wQaz3qojF2DT86mE_Q_2#nT)12MkmLyj&FYGOjl2Ks2`+k($*mHjw=gXCceXU4C z!OoYH&qViVLax_u-pZLj&fIMHaD&{()RXMi@QP^%x!p!WT98w7Y=p;o35M2|!`q*gnA&?jKNN8_?yfJUJr}_eSlltg}vy5jz zv^dvyc_^F|85x5vZTR7piZO1irwR2u1!kdiJX z1*E&Xq`SMmdBFD<=Um_U7p`aTwP($mxaXc(?R6!xQP8_&78)8_aZELUe2)5D2j%+N z3V-LT;b-|hJ>i)~6&K$Vo+NSZCl8wm*NFlVwxPjW%KBGHJ}IZAkCY3pE2=fcd#oKT z5AW*~tbv23|MxqWfC!j(J|YCR4(KWBs-G{TtGt7~h$wr;IBR};cTwk{8DjA=k-7ch z&kb_n09i|Xjsh`?q{DRAqStNTgVmxyv$r=kOs%YRdM1(*+|<#Bk$46t-w66#ND>(& zNuq0%8%Zy8p+gf(wrf~G9Y%2oj-1{@G7t5o5WS5RXBzma)5Rra4?RUaiJlGsIO9EZ zvEwUBZ=qRZs+(|sR1vrlAXT93N1vg({~zD>H_n6E*!`~!z=Tz0T6@nOOcGs~4l(^raah zwc2Kz2aX8hpfT}aaWn(P^fMJOP(lRfdlIkLb#NGPaL{d+!hhR6eYc1GBlB z5!u-7hZw8q*zok7XY)`ULU}0JVrjUGb9rj0_MI`u&uOsqcwbvNRMD*7Q8*DP1HuVP-?dd@_2DKRmt-y{L{-8PF2V`dLsQq_~_Bs&naIgmfJa{PNKeRUV# z+va`54}suTYyap{RJ6d6oPR;^p7_CBvrI8@VWnPQC+g%H>2Ay#~SZoT2>{ z^1PU<)tH!gYV?N1c$Znl6W;daCy+)0PPp*`J>{jqUjaVUjBz<5G>A8 z^9lip5Z)%iVRiPWiWjoOVC2-_8~asB>PgUkxQ{sUC~2rb=mfGJSJPbDG4B+`x}!Lp zSO`9Bt#8R(ej|PRV|Ku-6Jxkbksf%QP7`ukF@L!orfuLRR3Ka7lH?}_q$Gdy0cSPkrREE_34*vVL6e-~j5LT~o zNUS}GPHNT|p!w@^=(eLXA-;uZD!eeeHqa`6L6RlE9gviBupcd4Rv%<`SS&NJ4+g{j z-_Mhyleh&0&%bm;x>o6At~ppm30*8cNjt!)x|SGJQly@!Z!@mv{Q&!oF~LnTKXUf} z{_y;cWs>t}AT*r`<@=(MU>~~xs5kq-eaj>$_y>GtBF~Mwa|4$T->!pJ;QrGJON&r2 z|DGm@4O&)CRM894eVhRQyafql#X5m>8TobSw7IYo(>{nAhq5Th8`1OePu}_KQ?Z4f zU{3NsjBl?oM_zpJ+n-wn3~^q8KQ1~_Dx}Kl@j0!7E}Ba+(Ef{1ODf#&z9Y^idKZiH z*=-KYv#(auar=DZDHk!(E%}bR=^m8i);wt!GhV@U+Z^0~jh6s6$dT?*jN$wQ6X#-6aFzDbx z5X;vKqe6at&A5Jsnm1Yf8&6X+(JG2(0u=4P6R~e7q#l`=%lWm)g*%V7amraZCq}!^ zfMSnXvEx6PbwHe>pBXiVu@{fC!8x3~n;1OX<0Mf(J5u}Tv@h*P875S|cV8rH+(!9v2uDYL zl_KfpID+N+?ZR?9Fn~4z>9}dkihD|S+p1ys^p%hKkL`~(CHujr9i3mEBc!c|=3YAsMrta2W*7`$hKf|h^Q?UpPf?L6?tToX~ zlz@ZL6120Bm)UT_6w#b0^&TOj%fjt}fGW<|%V#w~2eTmr%3_~820lu>Io#{Nm*j#e zHshi)KE4jjv_3HWC}3LW%@qOe{BZ78wP;Q?Mr!pt`t-g4EDY%Cr}iBQ7rK{ra(4J_ zj_r%9P|UgRa^H3D_i%55ck2rrAK*tC2>J)R|2;uUwZG=SAF(uU|5WvRCOJ>;_xCGy&t>M!r@g~XYOhqk*XFm^W8Hb6YROZEj-3u|wa7KI*W)JhwY#9hu? zF^kH+5>i^Z`o7g&^S#PTDyoZ!cB877yc|ubO>V!UHc2>WtPxJ*P|z9D-3p#oWKrBIocyw3W>sVT}TCjB~p zhT*3mXz})>pGkxC%lSN}de8g=C+&X+?D(>MKMYlk3Znt*A@Ku(tcdf0mukx!BOm+1 zyADnxa$S|rgi1LKa!4wNZa>XyN_MGwb@{@Plv#&%mkKZI>SQo-g6F41tC$%p7FPC7 zK#dVdQ}m~&W30@zL>n2;?77ufJ>eJs$CN;nP{KXT@~>&MQ^ZK)5tlwM4?(+t@J31p`k3zG-}?+MA35N}9i~BO+4{c?;7+Ax z&?RYTJ^!9Y{@P*KPe42uk#S{xBz17^v?@t}tK&{i969s7YR8fYEqWQlFZd<0xuFX4 z*T!Q2(Ez_nuyH)^eK5)@4ikH(Pq!QjAEu2Q^K1qcX+8Kjp9ax#0mC$#NpD<6qO5%O zis$q7^Jv?hUnT8j2hdtLokF*^207}76>jUjf?dR}Fg<16$db88Qgx69irH@+kFm?r zfpu1ox}K`euYNf6Rq72e;LuU$5SyaYA;j?V2amY$w`5I@rH~J+;~WOho>D(mOOa1W zPQGvDJb~w-z2LErln5|9SnuP ziWh;}fnJ?Js)U$UgqiF<-SW$^tyr{5no|g7!+PbkIWDCWi`R2b43eRhy zWtQKAFX`fZ>3e=Tme5%iF4N-7ZWEnJP43DzrTp&bvfqdS!z0`&IF>4H*hfUH7T8vQ z(KRb7C5c>L)eUTXI>6VXs!X2MshmNqO*x& z`#rH_!}07s^YnUWwkgs?Tnlv>Vmg5!0;f^B#AysC}%ji3rx0xH=jKY%+3R4>D^(!+%=6I7JdDP@dkM^B!pcMsXhg8A6f=nWz zf7gWlI1GA%F1oa3b73zopaTRht4~NwSjtKkWWnZ}eT4wy78A{EuiY?_f1br1+DHnH z=8Cp7AG+;Ay_7k*p&@*rBWlmJnhj+Cnw^h-i*mv?iI^+z z=TGgGKWpQ=Ru&^qO|?uuWaT!Af~bCRYn*ok5sMCwj2s^v2tf%(F#}Q(&cB&{Wmp(< zX|~a|d7oGzV|y8_`EJ9f<5IDh_2!L#me1i1qYz*~fS2e%yNcQHt0}N%=Mij;fENNY zELhP#VvSsF*vSH7MQemxaua8!^6BlnMJyk6y>+6Yr~4NG;y|c*Q$~36?C@rO#%jtd zARk9Cy~(wXe`x*GX1@)ZdX2i1yRaU=l#1u&q?&zDRU9*2pmctsSAu$dsp7;OjjOEa zr0d{1B_&RwL7!2Bzey8Ml&G*%N%uEh&5?>@XrYB7&0B5lK7%4z8F4eHq>@_g5_35o zK6qccPJ+*DKyj%BbDNRuFtuS&n^pblcT?TDjFDsR_bTMVzYf7h!f_!dnnMtii>qVr zyHv)so-Bo_aC(0{G?ImEF=;qRP(K;TFi-3dLcd8LYaI6ug zMPbXqSOn3f?M|2L5}*Bp>v0(q%o!RXrO(?S0xR~|5EijLLtk^m&#W(x_q1{@^#e2bH_ zQ-^yzrTp_z_Msw$K%UGrsYcbsm_);z z?CQJkg5~y)#s?erlM2Eb_?;C$kQ2|oEI zR(st)Ff|8Ka}?S)vwlP^w8+5>8yMo%++vuy_u7JpIY2jm0CjXT z^CRK1j#Z$A!x%OThvWDnHl}DO3{0DosH&<~TlmkZi_weLWu2UM2b3W)*{){?y?^R1 z+%R)q>j4*ZE)f$G!c-E(Ag}oBb-0-tbj|cllQm+-kGZ0?wb1&wx?bBIm(x-QetyDZ zU`^4K@5o0N7Rr9`fEo}S1pn7gln)A6d@D6J((d1(gyKV2Z?DOA3!S5R zMF@tQCzG=Tp3VR`?RRiN8iNJ|XbU1$TLgj~uXsnslvDnt`y z+Vc)a9%Pq@ItGZHvF+phsKav=S08C9HOCGPA8Ri}FCX+1{@0J8S8Zwpu$csLt4^AW z{yRWa#D!N>R4>9oej$jW%ZuyW;Fr*XUXGT2xqG3B62CZ>^k!%eqMQ4dTSYV9U%>c8 zkiNr9i4iq9@~@dBIJc>qzlM@#9g8%DP!6Mk`oe;}NKt-|p4D!*{^+~VmEb_YbaJx! zpn&}~zr^UyRFcW(Bj2SfDZobX3KOvC?Y^the;1K)sXB}$>gHUgs-DT}Kc~iZu`5Fc z6y>F3ac#9(SCtWT*-Lt|DSfM)K?hSY|49zX5G!ry=%+6;|ER>YLv3T(UGXQEdA>fz zy~DcI)3`#c#KvFIGhKTVg{sB@l+)7Xyde;nD!jSi6wCVvL@IDN7Ye=NFle%#q3yH; z0IncDEA=9@Dtv$@ba*!5GI)(c{c63+R`SC8Q#KTr`wCFNcyzCa(e+|AISNJ|hSAP; zrrlS)f*taL)V?N#?m6}buiDP?y)VuC7ZW1H?+=LphBOou6E|RH2!t)xnKV=HpC33( zfMr5F%hrXkImQo*!_UD5x?4TuikNU^Sl8PE%lLBdgl#3775@_uX2ty{QgJl}`nDf5`(B|wQ*mfz zM5NDtYsa3`baWxK4)=39H9O7p5959kIe=SdhbOsTj<5r0x*xe%9iB9cRg6D zlsN$S1*V`h2201l1ZCb;pTRkxSz&($Z8IN6H-5TI zoELPAPvh2{?Q9HoCwir1F6VHrdyW3+jWy^8IyKHEw|b@FHur&MREVuP@hn+E{>buZ z1ksDI701CQ(|H8#d1K@=86WMFE*XdYIG(cqbI#^g+57X?gQzeSbDovm*|8zw$+f1g z#=DDL=jLmnv)wy8xfKpBj%yrP#eS-1^L%BSBpX=6sd|m5d?!rDu{Nl=EbpGZ6>`~% z&dDW=N~#<)o27B%lfLhU?IRrfBTHVp@hl839x9uCn@e)-}e zPPP*8T!6+NPv7;3HC@NoN434(KNm4Y6ZlF~OG?BaIOv+HoR0fP;j&@l!};4!ar2Ew zx`{Q7%;s7t^8F@UFC?OzD&3DDTI*O-gDl$G4x*3zUk$BVeNfxlsgRGFuayp%5J}nU z9}g$QVnDR#sTaC1Ji>=d8!{5e{TMo8kF#O$?F{;iKQ@PpdFpw`xSgx8S-H#T`zY%A zgeL}nvgNjqNwnYoctW2z_;#J)Nk6T~7ZL4sb;YN}5ww()6&^d|_=`%D@6ph^9k|Zc z^K=W^$L^tx-c=dzVCmo~n23xuE-2Wp7>jk_p z1!IpWZ$273V=;*mcg&w-+f@;thBUF|?Ft4jn#GCuABeU%hWZ@K3DA5a82u*bfc{(6qynWX^z7_-U0E^E8GFvX zatkJ35OA`1u!k67>7dSGKKC5*HBpXiYkT~ZMMld>E4aOO4@b!~*X?6{iGcxGksj}+glG1Yu8&~bGVaIl@DAYe&L3#gH1 z*`f|YGnry&W<_%fMZzmze1v06$I>k|=19Cs)2FO8`i3+I2MvoqS**pew>zb0eU#(T zpt#QRu|PNvRh=}`c_4+8*LVO@2&=7WJQlGvW;>Fbt!UaWES-=19=+OcThnb7L+J_% zG?eFL>I(AJq8=6eI*fGtbw;@+iqS5H(wLNLv(@)7V0c*X@Gws5DDe05(KG<~GS>h69IOi^UFO#ECGSy<@1hX>Azy59*b|8~T5>E0Ccf_}UA+qG(#PnwEl zb=l3|g?#Nl_>3)z9vB&C^Y>4VKiS0V*Gu6sMp!JYuXHW8S*}@};@%y8UPYGjZF6<1 zmIP%R@0(u7hnqJ!!@}HhNr)fE^(W)HZH(DyO2wUg$y`>i?)LdL*smGmh4}?bWYDr0 zJNMJVe$wQi?;wkShu=|}wu>-Xm1u?!X6|-aI>{wa?F9i_BiDgh8}6OFL%^ zU79ansi*8LElql$v8841FZ3-F*k5^W&&Z@w>y689_Lo>Uvb??;m-pC)UbWX?@`k&NTyMWYxyQ@h_YO3(1G{xMCk-8l%_z2t=y>1aHwmV0uB>ExOgYZS zmcS$~j1~j)lIK^hVIPzBOXWU&%UHZitoKb|+8v)Ymf_k8cmv|}bf>3U&rR;e^5o*Q z8cCt{?Z|b}ocnY7d8pk#$1fITJ7ad1|M7*NEag@&Vc6^4h*yMRLe%8)zl$fz^9dQ# zi+wOZ#O>-8AWoUYC#O((RD5gUNF?@pr(uw$+RGPcQL{pxz>aoUQE_pxLS9ICV=Rv zT1BafdPPHOy!BY2K0Z&mcLe`4C&d#TkBy4bWWO|~P8IQ!o=`>nA$MMhAlLT5RaX?N zu!C0`i@~;Ix1wm^;WVM73em$kQaW3 zbwrC&YtlsaBy}CsN-qEIHSScy5Q9&)i)YyJ^+L&Cgd9VkewY@^kQ5K|;OB5RIHElx zbL3(Sfv0#H@kXD7Q>9bpqd0}S%hfm4Jn2lizVa@*avNl5y6RXlze0^Tk_$;pX^OH^ zWV?UA4iy*&+sPuy;kdKKNS}>FU4Fx+52rbs)~=kq@B_4*%JG%jrXs4@cLLkuY-YQv z_F!@m`%sf0kB~yQdP`uNo;Z=*8~6TrNjt3W< zJF?qbyYa=5|FaBAO8fw?z-Tk*gI$8&kc0{e9i7Sc*i8Gq8J9D3N+yiIlhsat|2`rQ z4w^dXS2fYm<>cq4u6Jwx#C8`n@xDola5!6#hKZF}G9W!eebhz9F zuLrBX1wCm+<(y594{zc>FkOA(d(swSIdU@enE2o6?#hvlDViS+V%_S9H#t)F9I&w~EasVr&UXjG%0;RQwW@nV3Cp_WtEL-Dq+zzJ?R0Y zlQlw!(~~>*GNW4Ia@E3h7NyoFL@=rDfzZmK_l?_cSf`@8wqrSy{m5^mh?9ja-$lKh zuO83DmE?h%E-D%=x7jF0Fr0}^IvAZU$m%mVGLhie0)6=VtH)+*qvCj)#YUwbwehg% z(6Cv7HhJZ_iM(kCMK#MvAzJz^IhA$(x}p9onfQC9rN`afahUD?{8j?rldevP#{Nu{ z=cR9J9CA6NkQY|Gbspy3gB=UUnqeV{Fsz&S_Sg7UTA2C0Lh#7FC`kxA zC@VK2q9Kcxx9<`1(pE<9Xh7nzGpbKUv*wPgkXqePT>yiSI-Q0V={k>6e6oa zb3>fcr;ABfmd6WD!cDZTMFL2Nl2e}X-mk@X2fsUYZ?os_XQ`s; zvmr6+H24$o*l#rSd!U~>stdRi&0yfNeYAZUf&YhqAnTuut*r;VaaPaUxlHy~M+nqz zBUatoW+mc(Qm);w5Azb{=B!l~x}VV6q&4n6HQ?)i-z2THEywYJ;y}s9)3>9X=Rn3j z+JjeO3Pa6?bmDgq>QzI3vz-;OqR8Lhe9PWFN7(W|n{u!AoCCeL7mBqS2(O`{V2+dQ zy9O{Ct6Zd%aPC9GI zJeSpt?AsR}$jp(@5;s;fr-CPQ< zHFt8=w7uoO69aR!3o9t zxyC;KjVGPbX9hkQ8@nok*H1-ro!5r=JiB*qV)H5=w%-lrtX)aIxEeOO21{B3n1qCk zgXu{4lMW5dJ2y8M+LQ82OVAB`%@>lIR_!t({l4P7TtsMXtFpl{$yB%Aum7wt64^o*^VA4za2Lafb<#q0E;lb-Y6C@`sR; zIs0d0bNA)Jzu%~ywtH{%sp@l5t@vlqoWEB0LJTZ6)XT~ELcGO1Bwxs6-ealVA4|(g zC)z%x^~=bhXAEDqY3Q_UM2cwn(emy*Y-Bn|WA4_d+YKhI_uE&Lo}3NO{3k$svCZdIa3h2rX1#&Jm&)@d+j;TnhL52hZ=-0Be0kIAWh1GqSTLo@ zU=3D8O5g9&1c6fglpZ{2<_o@zUWUZVKdi9ov+$15;BR?o$B1N6_03HdOzulQd@L^< z_*EY7j>-Aqjj9wh$PwRfN|7lwH8RFzWMsgmFAyIcACC+UK6lxf$||#;&$&&?ZDMoMPHEoL8N-|ehDuo<8u9K z!^O%ztH+ZON?l6Fa>Mq+go$s}Td^gv?evE^Pgi%G3{gWHI=KvfJ2JBO9hrd-?h|~ zvt*8ReM;v2ro}HYSWL;N+-0z~XqQ3>S)V#=D2Kbq+3I##EP;ZC#>K%A867R?O~jerE2u!&t4fRBt<023sVJS6C@c4^^kP81M92||4SOXv}O*2?LC=G0mUSbmROw_NI6+uhh-5!%%DKV z5tuP(+cDHuv`q!e!qKYBa%l7Cv=3kHr?t%be0EH)_+SUMiS&`IvS00#rhU# zKc@+^M*)mWwA0whhuj!bQ6SAvmg3uMfmu&_Od_I;hl;O#qiHL@{1t{MWoM8ssic1C zba!9SZcDDeVqWpx9N3x+c7HYXTbu5eYNfyVYQy2r8J{?jKD8O~POWp9NXFagXND?Y zJ;=b)*!C(Z@{Cw_6`!AQSD`bnO8u~JM^`KJSVCfbTK*tp$Sv-@nc0)@7OxMKv)^N4 zVqiECn7MBw2JZi3b@B+EQdCJR3}%)4{bC8*eKI+({b%^sxVSjAL)v4CPiCZao^y-m zS$}8(hDmqiBwi?eaD#lyqN27DIho@f7mEFRqx(vEn^C`5mecuQS+@v3Qn6!e-h?Po z%ItO|_4CQnYsx%N?RI+`5-96#7X@IdGWO~*(7Hd}mKmzckrUP$)t^RZFV=)|(dh(504g365kVpI^^*EyQqhHIc zK*=+F{$av2Pg$ZA{dihi)8aNCvnT2*GkI|X~Q>3G*E=#RAmghR3<;5_;R^1 zO&CMwjPFmy<%Od>tkyYn(ub1i?;&!>SP$?$gXxW}yAj1>#@%zeeWgcBccOU<#!|CW zj@`L6P$Mz;@!tGy5{PG4vg7?uCdGPW?*%v9z`Jn27(*xkW|!XD<7OE38?K4^gaCPS zgBJP~??>%SHZ%lyd7E(sWpxv-?l@-thD_;{=vby1kP_dvn7f*jJrwWyqP+V2uhjRV zd$kfNuCX{dx0WGo#E6E{ESPqF_ZLNFSSfEG`3gMfZai)NBH_yz5b)eW+_& z$MnuLI1qT>T^6czvP+emTe3rsl z9)*&S(o}#)hw-sN`d`?J>$coEugU-fq_z^mmIz@nK5D*b% zp34(mBh^E~=LZW803;>O=#R`F)IF~;!&|ODEo5EfY&%#r>##YcFxAF{ZvJzxC4s7+ zFNj-9C0Uo4{Ow}`nE!{Xm42x2f0LFm$b0=yOeI*yjJHGTnL0cKLhkVG6y|Ck#=Y}@ zUkxB}KP}ebLJiEgW!pA7lz$t4_WM(g9zlf>u=HM2H|`<-`1v!Ub_%)F?saP)D@3_J zYhMmD8N&Wc+1YY6jG@Cq@Fp#NI2`(&>g}g(>bJew&+Was!c-**g!#INNlCG3Lb*pj zBfah5`;5R*K?cGqZA*^7(jqgkk7)ZSwO$OT&wDJN ziC36-ZDP%3wPMn25`T;GkJi_tN>j3jO?n1<|;AEcJ{X&n#4}y!! z%N|HH{^gOhuG^iw02x3aOjLkNQ)2#}GI_@7R!U}m6n!IJw)!Bc!%&K%~9)w;g~}^?4+@I zk%xxXg=2SucM-)qsiME4(6}bzg86^k~5vObPYk0zRu2{HX{3wh12oRR?VW{tJxm$CyN#poK7MK?|kg2i=u6Q@o?nk#1Bd(p=!6DtJ0Y4&t``B z

+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

Xt9JP_91{;3jCU`omrol2CVwe@##OSN_h-p%zOh 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 + setup, "Download", DownloaderDBPragma, DatabaseBodyDBInit); + + path_ = path; + + auto db = conn_->checkoutDb(); + + boost::optional pathFromDb; + + *db << "SELECT Path FROM Download WHERE Part=0;", soci::into(pathFromDb); + + // Try to reuse preexisting + // database. + if (pathFromDb) + { + // Can't resuse - database was + // from a different file download. + if (pathFromDb != path.string()) + { + *db << "DROP TABLE Download;"; + } + + // Continuing a file download. + else + { + boost::optional size; + + *db << "SELECT SUM(LENGTH(Data)) FROM Download;", soci::into(size); + + if (size) + file_size_ = size.get(); + } + } +} + +// This is called from message::payload_size +inline std::uint64_t +DatabaseBody::size(value_type const& body) +{ + // Forward the call to the body + return body.size(); +} + +// We don't do much in the reader constructor since the +// database is already open. +// +template +DatabaseBody::reader::reader( + boost::beast::http::header&, + value_type& body) + : body_(body) +{ +} + +// We don't do anything with content_length but a sophisticated +// application might check available space on the device +// to see if there is enough room to store the body. +inline void +DatabaseBody::reader::init( + boost::optional const& /*content_length*/, + boost::system::error_code& ec) +{ + // The connection must already be available for writing + assert(body_.conn_); + + // The error_code specification requires that we + // either set the error to some value, or set it + // to indicate no error. + // + // We don't do anything fancy so set "no error" + ec = {}; +} + +// This will get called one or more times with body buffers +// +template +std::size_t +DatabaseBody::reader::put( + ConstBufferSequence const& buffers, + boost::system::error_code& ec) +{ + // This function must return the total number of + // bytes transferred from the input buffers. + std::size_t nwritten = 0; + + // Loop over all the buffers in the sequence, + // and write each one to the database. + for (auto it = buffer_sequence_begin(buffers); + it != buffer_sequence_end(buffers); + ++it) + { + boost::asio::const_buffer buffer = *it; + + body_.batch_.append( + static_cast(buffer.data()), buffer.size()); + + // Write this buffer to the database + if (body_.batch_.size() > FLUSH_SIZE) + { + bool post = true; + + { + std::lock_guard lock(body_.m_); + + if (body_.handler_count_ >= MAX_HANDLERS) + post = false; + else + ++body_.handler_count_; + } + + if (post) + { + body_.strand_->post( + [data = body_.batch_, this] { this->do_put(data); }); + + body_.batch_.clear(); + } + } + + nwritten += it->size(); + } + + // Indicate success + // This is required by the error_code specification + ec = {}; + + return nwritten; +} + +inline void +DatabaseBody::reader::do_put(std::string data) +{ + using namespace boost::asio; + + { + std::unique_lock lock(body_.m_); + + // The download is being halted. + if (body_.closing_) + { + if (--body_.handler_count_ == 0) + { + lock.unlock(); + body_.c_.notify_one(); + } + + return; + } + } + + auto path = body_.path_.string(); + uint64_t rowSize; + soci::indicator rti; + + uint64_t remainingInRow; + + auto db = body_.conn_->checkoutDb(); + + auto be = dynamic_cast(db->get_backend()); + BOOST_ASSERT(be); + + // This limits how large we can make the blob + // in each row. Also subtract a pad value to + // account for the other values in the row. + auto const blobMaxSize = + sqlite_api::sqlite3_limit(be->conn_, SQLITE_LIMIT_LENGTH, -1) - + MAX_ROW_SIZE_PAD; + + auto rowInit = [&] { + *db << "INSERT INTO Download VALUES (:path, zeroblob(0), 0, :part)", + soci::use(path), soci::use(body_.part_); + + remainingInRow = blobMaxSize; + rowSize = 0; + }; + + *db << "SELECT Path,Size,Part FROM Download ORDER BY Part DESC " + "LIMIT 1", + soci::into(path), soci::into(rowSize), soci::into(body_.part_, rti); + + if (!db->got_data()) + rowInit(); + else + remainingInRow = blobMaxSize - rowSize; + + auto insert = [&db, &rowSize, &part = body_.part_, &fs = body_.file_size_]( + auto const& data) { + uint64_t updatedSize = rowSize + data.size(); + + *db << "UPDATE Download SET Data = CAST(Data || :data AS blob), " + "Size = :size WHERE Part = :part;", + soci::use(data), soci::use(updatedSize), soci::use(part); + + fs += data.size(); + }; + + while (remainingInRow < data.size()) + { + if (remainingInRow) + { + insert(data.substr(0, remainingInRow)); + data.erase(0, remainingInRow); + } + + ++body_.part_; + rowInit(); + } + + insert(data); + + bool const notify = [this] { + std::lock_guard lock(body_.m_); + return --body_.handler_count_ == 0; + }(); + + if (notify) + body_.c_.notify_one(); +} + +// Called after writing is done when there's no error. +inline void +DatabaseBody::reader::finish(boost::system::error_code& ec) +{ + { + std::unique_lock lock(body_.m_); + + // Wait for scheduled DB writes + // to complete. + if (body_.handler_count_) + { + auto predicate = [&] { return !body_.handler_count_; }; + body_.c_.wait(lock, predicate); + } + } + + auto db = body_.conn_->checkoutDb(); + + soci::rowset rs = + (db->prepare << "SELECT Data FROM Download ORDER BY PART ASC;"); + + std::ofstream fout; + fout.open(body_.path_.string(), std::ios::binary | std::ios::out); + + // iteration through the resultset: + for (auto it = rs.begin(); it != rs.end(); ++it) + fout.write(it->data(), it->size()); + + // Flush any pending data that hasn't + // been been written to the DB. + if (body_.batch_.size()) + { + fout.write(body_.batch_.data(), body_.batch_.size()); + body_.batch_.clear(); + } + + fout.close(); +} + +} // namespace ripple diff --git a/src/ripple/net/impl/DatabaseDownloader.cpp b/src/ripple/net/impl/DatabaseDownloader.cpp new file mode 100644 index 00000000000..46f82d6a757 --- /dev/null +++ b/src/ripple/net/impl/DatabaseDownloader.cpp @@ -0,0 +1,88 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +namespace ripple +{ + +DatabaseDownloader::DatabaseDownloader( + boost::asio::io_service & io_service, + beast::Journal j, + Config const & config) + : SSLHTTPDownloader(io_service, j, config) + , config_(config) + , io_service_(io_service) +{ +} + +auto +DatabaseDownloader::getParser(boost::filesystem::path dstPath, + std::function complete, + boost::system::error_code & ec) -> std::shared_ptr +{ + using namespace boost::beast; + + auto p = std::make_shared>(); + p->body_limit(std::numeric_limits::max()); + p->get().body().open( + dstPath, + config_, + io_service_, + ec); + if(ec) + { + p->get().body().close(); + fail(dstPath, complete, ec, "open"); + } + + return p; +} + +bool +DatabaseDownloader::checkPath(boost::filesystem::path const & dstPath) +{ + return dstPath.string().size() <= MAX_PATH_LEN; +} + +void +DatabaseDownloader::closeBody(std::shared_ptr p) +{ + using namespace boost::beast; + + auto databaseBodyParser = std::dynamic_pointer_cast< + http::response_parser>(p); + assert(databaseBodyParser); + + databaseBodyParser->get().body().close(); +} + +uint64_t +DatabaseDownloader::size(std::shared_ptr p) +{ + using namespace boost::beast; + + auto databaseBodyParser = std::dynamic_pointer_cast< + http::response_parser>(p); + assert(databaseBodyParser); + + return databaseBodyParser->get().body().size(); +} + +} // ripple diff --git a/src/ripple/net/impl/SSLHTTPDownloader.cpp b/src/ripple/net/impl/SSLHTTPDownloader.cpp index bf8f9962b9b..3bf0622fab2 100644 --- a/src/ripple/net/impl/SSLHTTPDownloader.cpp +++ b/src/ripple/net/impl/SSLHTTPDownloader.cpp @@ -25,10 +25,13 @@ namespace ripple { SSLHTTPDownloader::SSLHTTPDownloader( boost::asio::io_service& io_service, beast::Journal j, - Config const& config) - : ssl_ctx_(config, j, boost::asio::ssl::context::tlsv12_client) + Config const& config, + bool isPaused) + : j_(j) + , ssl_ctx_(config, j, boost::asio::ssl::context::tlsv12_client) , strand_(io_service) - , j_(j) + , isStopped_(false) + , sessionActive_(false) { } @@ -41,21 +44,8 @@ SSLHTTPDownloader::download( boost::filesystem::path const& dstPath, std::function complete) { - try - { - if (exists(dstPath)) - { - JLOG(j_.error()) << - "Destination file exists"; - return false; - } - } - catch (std::exception const& e) - { - JLOG(j_.error()) << - "exception: " << e.what(); + if (!checkPath(dstPath)) return false; - } if (!strand_.running_in_this_thread()) strand_.post( @@ -84,6 +74,24 @@ SSLHTTPDownloader::download( return true; } +void +SSLHTTPDownloader::onStop() +{ + std::unique_lock lock(m_); + + isStopped_ = true; + + if(sessionActive_) + { + // Wait for the handler to exit. + c_.wait(lock, + [this]() + { + return !sessionActive_; + }); + } +} + void SSLHTTPDownloader::do_session( std::string const host, @@ -98,126 +106,231 @@ SSLHTTPDownloader::do_session( using namespace boost::beast; boost::system::error_code ec; - ip::tcp::resolver resolver {strand_.context()}; - auto const results = resolver.async_resolve(host, port, yield[ec]); - if (ec) - return fail(dstPath, complete, ec, "async_resolve"); + bool skip = false; - try - { - stream_.emplace(strand_.context(), ssl_ctx_.context()); - } - catch (std::exception const& e) + ////////////////////////////////////////////// + // Define lambdas for encapsulating download + // operations: + auto connect = [&](std::shared_ptr parser) { - return fail(dstPath, complete, ec, - std::string("exception: ") + e.what()); - } + uint64_t const rangeStart = size(parser); - ec = ssl_ctx_.preConnectVerify(*stream_, host); - if (ec) - return fail(dstPath, complete, ec, "preConnectVerify"); + ip::tcp::resolver resolver {strand_.context()}; + auto const results = resolver.async_resolve(host, port, yield[ec]); + if (ec) + return fail(dstPath, complete, ec, "async_resolve", parser); - boost::asio::async_connect( - stream_->next_layer(), results.begin(), results.end(), yield[ec]); - if (ec) - return fail(dstPath, complete, ec, "async_connect"); + try + { + stream_.emplace(strand_.context(), ssl_ctx_.context()); + } + catch (std::exception const& e) + { + return fail(dstPath, complete, ec, + std::string("exception: ") + e.what(), parser); + } - ec = ssl_ctx_.postConnectVerify(*stream_, host); - if (ec) - return fail(dstPath, complete, ec, "postConnectVerify"); + ec = ssl_ctx_.preConnectVerify(*stream_, host); + if (ec) + return fail(dstPath, complete, ec, "preConnectVerify", parser); - stream_->async_handshake(ssl::stream_base::client, yield[ec]); - if (ec) - return fail(dstPath, complete, ec, "async_handshake"); + boost::asio::async_connect( + stream_->next_layer(), results.begin(), results.end(), yield[ec]); + if (ec) + return fail(dstPath, complete, ec, "async_connect", parser); - // Set up an HTTP HEAD request message to find the file size - http::request req {http::verb::head, target, version}; - req.set(http::field::host, host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + ec = ssl_ctx_.postConnectVerify(*stream_, host); + if (ec) + return fail(dstPath, complete, ec, "postConnectVerify", parser); - http::async_write(*stream_, req, yield[ec]); - if(ec) - return fail(dstPath, complete, ec, "async_write"); + stream_->async_handshake(ssl::stream_base::client, yield[ec]); + if (ec) + return fail(dstPath, complete, ec, "async_handshake", parser); - { - // Check if available storage for file size - http::response_parser p; - p.skip(true); - http::async_read(*stream_, read_buf_, p, yield[ec]); + // Set up an HTTP HEAD request message to find the file size + http::request req {http::verb::head, target, version}; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // Requesting a portion of the file + if (rangeStart) + { + req.set(http::field::range, + (boost::format("bytes=%llu-") % rangeStart).str()); + } + + http::async_write(*stream_, req, yield[ec]); if(ec) - return fail(dstPath, complete, ec, "async_read"); - if (auto len = p.content_length()) + return fail(dstPath, complete, ec, "async_write", parser); + { - try + // Check if available storage for file size + http::response_parser p; + p.skip(true); + http::async_read(*stream_, read_buf_, p, yield[ec]); + if(ec) + return fail(dstPath, complete, ec, "async_read", parser); + + // Range request was rejected + if(p.get().result() == http::status::range_not_satisfiable) { - if (*len > space(dstPath.parent_path()).available) + req.erase(http::field::range); + + http::async_write(*stream_, req, yield[ec]); + if(ec) + return fail(dstPath, complete, ec, + "async_write_range_verify", parser); + + http::response_parser p; + p.skip(true); + + http::async_read(*stream_, read_buf_, p, yield[ec]); + if(ec) + return fail(dstPath, complete, ec, + "async_read_range_verify", parser); + + // The entire file is downloaded already. + if(p.content_length() == rangeStart) + skip = true; + else + return fail(dstPath, complete, ec, + "range_not_satisfiable", parser); + } + else if (rangeStart && + p.get().result() != http::status::partial_content) + { + ec.assign(boost::system::errc::not_supported, + boost::system::generic_category()); + + return fail(dstPath, complete, ec, + "Range request ignored", parser); + } + else if (auto len = p.content_length()) + { + try + { + if (*len > space(dstPath.parent_path()).available) + { + return fail(dstPath, complete, ec, + "Insufficient disk space for download", parser); + } + } + catch (std::exception const& e) { return fail(dstPath, complete, ec, - "Insufficient disk space for download"); + std::string("exception: ") + e.what(), parser); } } - catch (std::exception const& e) + } + + if(!skip) + { + // Set up an HTTP GET request message to download the file + req.method(http::verb::get); + + if (rangeStart) { - return fail(dstPath, complete, ec, - std::string("exception: ") + e.what()); + req.set(http::field::range, + (boost::format("bytes=%llu-") % rangeStart).str()); } } - } - // Set up an HTTP GET request message to download the file - req.method(http::verb::get); - http::async_write(*stream_, req, yield[ec]); - if(ec) - return fail(dstPath, complete, ec, "async_write"); + http::async_write(*stream_, req, yield[ec]); + if(ec) + return fail(dstPath, complete, ec, "async_write", parser); + + return true; + }; - // Download the file - http::response_parser p; - p.body_limit(std::numeric_limits::max()); - p.get().body().open( - dstPath.string().c_str(), - boost::beast::file_mode::write, - ec); - if (ec) + auto close = [&](auto p) { - p.get().body().close(); - return fail(dstPath, complete, ec, "open"); - } + closeBody(p); - http::async_read(*stream_, read_buf_, p, yield[ec]); - if (ec) + // Gracefully close the stream + stream_->async_shutdown(yield[ec]); + if (ec == boost::asio::error::eof) + ec.assign(0, ec.category()); + if (ec) + { + // Most web servers don't bother with performing + // the SSL shutdown handshake, for speed. + JLOG(j_.trace()) << + "async_shutdown: " << ec.message(); + } + // The socket cannot be reused + stream_ = boost::none; + }; + + auto getParser = [&] { - p.get().body().close(); - return fail(dstPath, complete, ec, "async_read"); + auto p = this->getParser(dstPath, complete, ec); + if (ec) + fail(dstPath, complete, ec, "getParser", p); + + return p; + }; + + // When the downloader is being stopped + // because the server is shutting down, + // this method notifies a 'Stoppable' + // object that the session has ended. + auto exit = [this]() + { + std::lock_guard lock(m_); + sessionActive_ = false; + c_.notify_one(); + }; + + // end lambdas + //////////////////////////////////////////////////////////// + + { + std::lock_guard lock(m_); + sessionActive_ = true; } - p.get().body().close(); - // Gracefully close the stream - stream_->async_shutdown(yield[ec]); - if (ec == boost::asio::error::eof) - ec.assign(0, ec.category()); + if(isStopped_.load()) + return exit(); + + auto p = getParser(); if (ec) + return exit(); + + if (!connect(p) || ec) + return exit(); + + if(skip) + p->skip(true); + + // Download the file + while (!p->is_done()) { - // Most web servers don't bother with performing - // the SSL shutdown handshake, for speed. - JLOG(j_.trace()) << - "async_shutdown: " << ec.message(); + if(isStopped_.load()) + { + close(p); + return exit(); + } + + http::async_read_some(*stream_, read_buf_, *p, yield[ec]); } - // The socket cannot be reused - stream_ = boost::none; JLOG(j_.trace()) << "download completed: " << dstPath.string(); + close(p); + exit(); + // Notify the completion handler complete(std::move(dstPath)); } -void +bool SSLHTTPDownloader::fail( boost::filesystem::path dstPath, std::function const& complete, boost::system::error_code const& ec, - std::string const& errMsg) + std::string const& errMsg, + std::shared_ptr parser) { if (!ec) { @@ -230,18 +343,21 @@ SSLHTTPDownloader::fail( errMsg << ": " << ec.message(); } + if (parser) + closeBody(parser); + try { remove(dstPath); } catch (std::exception const& e) { - JLOG(j_.error()) << - "exception: " << e.what(); + JLOG(j_.error()) << "exception: " << e.what() + << " in function: " << __func__; } complete(std::move(dstPath)); -} - + return false; +} }// ripple diff --git a/src/ripple/net/uml/interrupt_sequence.pu b/src/ripple/net/uml/interrupt_sequence.pu new file mode 100644 index 00000000000..ba046d084f8 --- /dev/null +++ b/src/ripple/net/uml/interrupt_sequence.pu @@ -0,0 +1,233 @@ +@startuml + + +skinparam shadowing false + +/' +skinparam sequence { + ArrowColor #e1e4e8 + ActorBorderColor #e1e4e8 + DatabaseBorderColor #e1e4e8 + LifeLineBorderColor Black + LifeLineBackgroundColor #d3d6d9 + + ParticipantBorderColor DeepSkyBlue + ParticipantBackgroundColor DodgerBlue + ParticipantFontName Impact + ParticipantFontSize 17 + ParticipantFontColor #A9DCDF + + NoteBackgroundColor #6a737d + + ActorBackgroundColor #f6f8fa + ActorFontColor #6a737d + ActorFontSize 17 + ActorFontName Aapex + + EntityBackgroundColor #f6f8fa + EntityFontColor #6a737d + EntityFontSize 17 + EntityFontName Aapex + + DatabaseBackgroundColor #f6f8fa + DatabaseFontColor #6a737d + DatabaseFontSize 17 + DatabaseFontName Aapex + + CollectionsBackgroundColor #f6f8fa + ActorFontColor #6a737d + ActorFontSize 17 + ActorFontName Aapex +} + +skinparam note { + BackgroundColor #fafbfc + BorderColor #e1e4e8 +} +'/ + +'skinparam monochrome true + +actor Client as c +entity RippleNode as rn +entity ShardArchiveHandler as sa +entity SSLHTTPDownloader as d +database Database as db +collections Fileserver as s + +c -> rn: Launch RippleNode +activate rn + +c -> rn: Issue download request + +note right of c + **Download Request:** + + { + "method": "download_shard", + "params": + [ + { + "shards": + [ + {"index": 1, "url": "https://example.com/1.tar.lz4"}, + {"index": 2, "url": "https://example.com/2.tar.lz4"}, + {"index": 5, "url": "https://example.com/5.tar.lz4"} + ] + } + ] + } +end note + +rn -> sa: Create instance of Handler +activate sa + +rn -> sa: Add three downloads +sa -> sa: Validate requested downloads + +rn -> sa: Initiate Downloads +sa -> rn: ACK: Initiating +rn -> c: Initiating requested downloads + +sa -> db: Save state to the database\n(Processing three downloads) + +note right of db + + **ArchiveHandler State (SQLite Table):** + + | Index | URL | + | 1 | https://example.com/1.tar.lz4 | + | 2 | https://example.com/2.tar.lz4 | + | 5 | https://example.com/5.tar.lz4 | + +end note + +sa -> d: Create instance of Downloader +activate d + +group Download 1 + + note over sa + **Download 1:** + + This encapsulates the download of the first file + at URL "https://example.com/1.tar.lz4". + + end note + + sa -> d: Start download + + d -> s: Connect and request file + s -> d: Send file + d -> sa: Invoke completion handler + +end + +sa -> sa: Import and validate shard + +sa -> db: Update persisted state\n(Remove download) + +note right of db + **ArchiveHandler State:** + + | Index | URL | + | 2 | https://example.com/2.tar.lz4 | + | 5 | https://example.com/5.tar.lz4 | + +end note + +group Download 2 + + sa -> d: Start download + + d -> s: Connect and request file + +end + +rn -> rn: **RippleNode crashes** + +deactivate sa +deactivate rn +deactivate d + +c -> rn: Restart RippleNode +activate rn + +rn -> db: Detect non-empty state database + +rn -> sa: Create instance of Handler +activate sa + +sa -> db: Load state + +note right of db + **ArchiveHandler State:** + + | Index | URL | + | 2 | https://example.com/2.tar.lz4 | + | 5 | https://example.com/5.tar.lz4 | + +end note + +sa -> d: Create instance of Downloader +activate d + +sa -> sa: Resume Download 2 + +group Download 2 + + sa -> d: Start download + + d -> s: Connect and request file + s -> d: Send file + d -> sa: Invoke completion handler + +end + +sa -> sa: Import and validate shard + +sa -> db: Update persisted state \n(Remove download) + +note right of db + **ArchiveHandler State:** + + | Index | URL | + | 5 | https://example.com/5.tar.lz4 | + +end note + +group Download 3 + + sa -> d: Start download + + d -> s: Connect and request file + s -> d: Send file + d -> sa: Invoke completion handler + +end + +sa -> sa: Import and validate shard + +sa -> db: Update persisted state \n(Remove download) + +note right of db + **ArchiveHandler State:** + + ***empty*** + +end note + +sa -> db: Remove empty database + +sa -> sa: Automatically destroyed +deactivate sa + +d -> d: Destroyed via reference\ncounting +deactivate d + +c -> rn: Poll RippleNode to verify successfull\nimport of all requested shards. +c -> rn: Shutdown RippleNode + +deactivate rn + +@enduml diff --git a/src/ripple/net/uml/states.pu b/src/ripple/net/uml/states.pu new file mode 100644 index 00000000000..b5db8ee48f4 --- /dev/null +++ b/src/ripple/net/uml/states.pu @@ -0,0 +1,69 @@ +@startuml + +state "Updating Database" as UD4 { + UD4: Update the database to reflect + UD4: the current state. +} +state "Initiating Download" as ID { + ID: Omit the range header to download + ID: the entire file. +} + +state "Evaluate Database" as ED { + ED: Determine the current state + ED: based on the contents of the + ED: database from a previous run. +} + +state "Remove Database" as RD { + RD: The database is destroyed when + RD: empty. +} + +state "Download in Progress" as DP + +state "Download Completed" as DC { + + state "Updating Database" as UD { + UD: Update the database to reflect + UD: the current state. + } + + state "Queue Check" as QC { + QC: Check the queue for any reamining + QC: downloads. + } + + [*] --> UD + UD --> QC +} + +state "Check Resume" as CR { + CR: Determine whether we're resuming + CR: a previous download or starting a + CR: new one. +} + +state "Resuming Download" as IPD { + IPD: Set the range header in the + IPD: HTTP request as needed. +} + +[*] --> ED : State DB is present at\nnode launch +ED --> RD : State DB is empty +ED --> CR : There are downloads queued +RD --> [*] + +[*] --> UD4 : Client invokes <>\ncommand +UD4 --> ID : Database updated +ID --> DP : Download started +DP --> DC : Download completed +DC --> ID : There **are** additional downloads\nqueued +DP --> [*] : A graceful shutdown is\nin progress +DC --> RD : There **are no** additional\ndownloads queued + +CR --> IPD : Resuming an interrupted\ndownload +IPD --> DP: Download started +CR --> ID : Initiating a new\ndownload + +@enduml diff --git a/src/ripple/rpc/ShardArchiveHandler.h b/src/ripple/rpc/ShardArchiveHandler.h index f1026603a8d..73ec512adaf 100644 --- a/src/ripple/rpc/ShardArchiveHandler.h +++ b/src/ripple/rpc/ShardArchiveHandler.h @@ -23,27 +23,76 @@ #include #include #include -#include +#include #include #include namespace ripple { +namespace test { class ShardArchiveHandler_test; } namespace RPC { /** Handles the download and import one or more shard archives. */ class ShardArchiveHandler - : public std::enable_shared_from_this + : public Stoppable + , public std::enable_shared_from_this { public: + + using pointer = std::shared_ptr; + friend class test::ShardArchiveHandler_test; + + static + boost::filesystem::path + getDownloadDirectory(Config const& config); + + static + pointer + getInstance(); + + static + pointer + getInstance(Application& app, Stoppable& parent); + + static + pointer + recoverInstance(Application& app, Stoppable& parent); + + static + bool + hasInstance(); + + bool + init(); + + bool + initFromDB(); + + ~ShardArchiveHandler() = default; + + bool + add(std::uint32_t shardIndex, std::pair&& url); + + /** Starts downloading and importing archives. */ + bool + start(); + + void + release(); + +private: + ShardArchiveHandler() = delete; ShardArchiveHandler(ShardArchiveHandler const&) = delete; ShardArchiveHandler& operator= (ShardArchiveHandler&&) = delete; ShardArchiveHandler& operator= (ShardArchiveHandler const&) = delete; - ShardArchiveHandler(Application& app); + ShardArchiveHandler( + Application& app, + Stoppable& parent, + bool recovery = false); - ~ShardArchiveHandler(); + void onStop () override; /** Add an archive to be downloaded and imported. @param shardIndex the index of the shard to be imported. @@ -52,13 +101,9 @@ class ShardArchiveHandler @note Returns false if called while downloading. */ bool - add(std::uint32_t shardIndex, parsedURL&& url); - - /** Starts downloading and importing archives. */ - bool - start(); + add(std::uint32_t shardIndex, parsedURL&& url, + std::lock_guard const&); -private: // Begins the download and import of the next archive. bool next(std::lock_guard& l); @@ -75,14 +120,21 @@ class ShardArchiveHandler void remove(std::lock_guard&); + void + doRelease(std::lock_guard const&); + + static std::mutex instance_mutex_; + static pointer instance_; + std::mutex mutable m_; Application& app_; - std::shared_ptr downloader_; + beast::Journal const j_; + std::unique_ptr sqliteDB_; + std::shared_ptr downloader_; boost::filesystem::path const downloadDir_; boost::asio::basic_waitable_timer timer_; bool process_; std::map archives_; - beast::Journal const j_; }; } // RPC diff --git a/src/ripple/rpc/handlers/DownloadShard.cpp b/src/ripple/rpc/handlers/DownloadShard.cpp index 174867ce1ee..02ff8e6df8e 100644 --- a/src/ripple/rpc/handlers/DownloadShard.cpp +++ b/src/ripple/rpc/handlers/DownloadShard.cpp @@ -77,7 +77,7 @@ doDownloadShard(RPC::JsonContext& context) // Validate shards static const std::string ext {".tar.lz4"}; - std::map archives; + std::map> archives; for (auto& it : context.params[jss::shards]) { // Validate the index @@ -94,7 +94,8 @@ doDownloadShard(RPC::JsonContext& context) if (!it.isMember(jss::url)) return RPC::missing_field_error(jss::url); parsedURL url; - if (!parseUrl(url, it[jss::url].asString()) || + auto unparsedURL = it[jss::url].asString(); + if (!parseUrl(url, unparsedURL) || url.domain.empty() || url.path.empty()) { return RPC::invalid_field_error(jss::url); @@ -116,16 +117,39 @@ doDownloadShard(RPC::JsonContext& context) } // Check for duplicate indexes - if (!archives.emplace(jv.asUInt(), std::move(url)).second) + if (!archives.emplace(jv.asUInt(), + std::make_pair(std::move(url), unparsedURL)).second) { return RPC::make_param_error("Invalid field '" + std::string(jss::index) + "', duplicate shard ids."); } } - // Begin downloading. The handler keeps itself alive while downloading. - auto handler { - std::make_shared(context.app)}; + RPC::ShardArchiveHandler::pointer handler; + + try + { + handler = RPC::ShardArchiveHandler::hasInstance() ? + RPC::ShardArchiveHandler::getInstance() : + RPC::ShardArchiveHandler::getInstance( + context.app, + context.app.getJobQueue()); + + if(!handler) + return RPC::make_error (rpcINTERNAL, + "Failed to create ShardArchiveHandler."); + + if(!handler->init()) + return RPC::make_error (rpcINTERNAL, + "Failed to initiate ShardArchiveHandler."); + } + catch (std::exception const& e) + { + return RPC::make_error (rpcINTERNAL, + std::string("Failed to start download: ") + + e.what()); + } + for (auto& [index, url] : archives) { if (!handler->add(index, std::move(url))) @@ -135,8 +159,13 @@ doDownloadShard(RPC::JsonContext& context) std::to_string(index) + " exists or being acquired"); } } + + // Begin downloading. if (!handler->start()) + { + handler->release(); return rpcError(rpcINTERNAL); + } std::string s {"Downloading shard"}; preShards = shardStore->getPreShards(); diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 805e156402a..edf0acbdf57 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -61,10 +61,12 @@ Json::Value doManifest (RPC::JsonContext&); Json::Value doNoRippleCheck (RPC::JsonContext&); Json::Value doOwnerInfo (RPC::JsonContext&); Json::Value doPathFind (RPC::JsonContext&); +Json::Value doPause (RPC::JsonContext&); Json::Value doPeers (RPC::JsonContext&); Json::Value doPing (RPC::JsonContext&); Json::Value doPrint (RPC::JsonContext&); Json::Value doRandom (RPC::JsonContext&); +Json::Value doResume (RPC::JsonContext&); Json::Value doPeerReservationsAdd (RPC::JsonContext&); Json::Value doPeerReservationsDel (RPC::JsonContext&); Json::Value doPeerReservationsList (RPC::JsonContext&); diff --git a/src/ripple/rpc/impl/ShardArchiveHandler.cpp b/src/ripple/rpc/impl/ShardArchiveHandler.cpp index 1405b52da1d..fda926ef22b 100644 --- a/src/ripple/rpc/impl/ShardArchiveHandler.cpp +++ b/src/ripple/rpc/impl/ShardArchiveHandler.cpp @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include @@ -31,41 +34,192 @@ namespace RPC { using namespace boost::filesystem; using namespace std::chrono_literals; -ShardArchiveHandler::ShardArchiveHandler(Application& app) - : app_(app) - , downloadDir_(get(app_.config().section( - ConfigSection::shardDatabase()), "path", "") + "/download") +std::mutex ShardArchiveHandler::instance_mutex_; +ShardArchiveHandler::pointer ShardArchiveHandler::instance_ = nullptr; + +boost::filesystem::path +ShardArchiveHandler::getDownloadDirectory(Config const& config) +{ + return get(config.section( + ConfigSection::shardDatabase()), "download_path", + get(config.section(ConfigSection::shardDatabase()), + "path", "")) / "download"; +} + +auto +ShardArchiveHandler::getInstance() -> pointer +{ + std::lock_guard lock(instance_mutex_); + + return instance_; +} + +auto +ShardArchiveHandler::getInstance(Application& app, + Stoppable& parent) -> pointer +{ + std::lock_guard lock(instance_mutex_); + assert(!instance_); + + instance_.reset(new ShardArchiveHandler(app, parent)); + + return instance_; +} + +auto +ShardArchiveHandler::recoverInstance(Application& app, Stoppable& parent) -> pointer +{ + std::lock_guard lock(instance_mutex_); + assert(!instance_); + + instance_.reset(new ShardArchiveHandler(app, parent, true)); + + return instance_; +} + +bool +ShardArchiveHandler::hasInstance() +{ + std::lock_guard lock(instance_mutex_); + + return instance_.get() != nullptr; +} + +ShardArchiveHandler::ShardArchiveHandler( + Application& app, + Stoppable& parent, + bool recovery) + : Stoppable("ShardArchiveHandler", parent) + , app_(app) + , j_(app.journal("ShardArchiveHandler")) + , downloadDir_(getDownloadDirectory(app.config())) , timer_(app_.getIOService()) , process_(false) - , j_(app.journal("ShardArchiveHandler")) { assert(app_.getShardStore()); + + if(recovery) + downloader_.reset(new DatabaseDownloader ( + app_.getIOService(), j_, app_.config())); } -ShardArchiveHandler::~ShardArchiveHandler() +bool +ShardArchiveHandler::init() { - std::lock_guard lock(m_); - timer_.cancel(); - for (auto const& ar : archives_) - app_.getShardStore()->removePreShard(ar.first); - archives_.clear(); + try + { + create_directories(downloadDir_); - // Remove temp root download directory + sqliteDB_ = std::make_unique( + downloadDir_ , + stateDBName, + DownloaderDBPragma, + ShardArchiveHandlerDBInit); + } + catch(std::exception const& e) + { + JLOG(j_.error()) << "exception: " << e.what() + << " in function: " << __func__; + + return false; + } + + return true; +} + +bool +ShardArchiveHandler::initFromDB() +{ try { - remove_all(downloadDir_); + using namespace boost::filesystem; + + assert(exists(downloadDir_ / stateDBName) && + is_regular_file(downloadDir_ / stateDBName)); + + sqliteDB_ = std::make_unique( + downloadDir_, + stateDBName, + DownloaderDBPragma, + ShardArchiveHandlerDBInit); + + auto& session{sqliteDB_->getSession()}; + + soci::rowset rs = (session.prepare + << "SELECT * FROM State;"); + + std::lock_guard lock(m_); + + for (auto it = rs.begin(); it != rs.end(); ++it) + { + parsedURL url; + + if (!parseUrl(url, it->get(1))) + { + JLOG(j_.error()) << "Failed to parse url: " + << it->get(1); + + continue; + } + + add(it->get(0), std::move(url), lock); + } + + // Failed to load anything + // from the state database. + if(archives_.empty()) + { + release(); + return false; + } } - catch (std::exception const& e) + catch(std::exception const& e) { - JLOG(j_.error()) << - "exception: " << e.what(); + JLOG(j_.error()) << "exception: " << e.what() + << " in function: " << __func__; + + return false; } + + return true; +} + +void +ShardArchiveHandler::onStop() +{ + std::lock_guard lock(m_); + + if (downloader_) + { + downloader_->onStop(); + downloader_.reset(); + } + + stopped(); } bool -ShardArchiveHandler::add(std::uint32_t shardIndex, parsedURL&& url) +ShardArchiveHandler::add(std::uint32_t shardIndex, + std::pair&& url) +{ + std::lock_guard lock(m_); + + if (!add(shardIndex, std::forward(url.first), lock)) + return false; + + auto& session{sqliteDB_->getSession()}; + + session << "INSERT INTO State VALUES (:index, :url);", + soci::use(shardIndex), + soci::use(url.second); + + return true; +} + +bool +ShardArchiveHandler::add(std::uint32_t shardIndex, parsedURL&& url, + std::lock_guard const&) { - std::lock_guard lock(m_); if (process_) { JLOG(j_.error()) << @@ -76,9 +230,12 @@ ShardArchiveHandler::add(std::uint32_t shardIndex, parsedURL&& url) auto const it {archives_.find(shardIndex)}; if (it != archives_.end()) return url == it->second; + if (!app_.getShardStore()->prepareShard(shardIndex)) return false; + archives_.emplace(shardIndex, std::move(url)); + return true; } @@ -107,16 +264,13 @@ ShardArchiveHandler::start() try { - // Remove if remnant from a crash - remove_all(downloadDir_); - // Create temp root download directory - create_directory(downloadDir_); + create_directories(downloadDir_); if (!downloader_) { // will throw if can't initialize ssl context - downloader_ = std::make_shared( + downloader_ = std::make_shared( app_.getIOService(), j_, app_.config()); } } @@ -130,12 +284,19 @@ ShardArchiveHandler::start() return next(lock); } +void +ShardArchiveHandler::release() +{ + std::lock_guard lock(m_); + doRelease(lock); +} + bool ShardArchiveHandler::next(std::lock_guard& l) { if (archives_.empty()) { - process_ = false; + doRelease(l); return false; } @@ -154,20 +315,28 @@ ShardArchiveHandler::next(std::lock_guard& l) return next(l); } - // Download the archive + // Download the archive. Process in another thread + // to prevent holding up the lock if the downloader + // sleeps. auto const& url {archives_.begin()->second}; - if (!downloader_->download( - url.domain, - std::to_string(url.port.get_value_or(443)), - url.path, - 11, - dstDir / "archive.tar.lz4", - std::bind(&ShardArchiveHandler::complete, - shared_from_this(), std::placeholders::_1))) - { - remove(l); - return next(l); - } + app_.getJobQueue().addJob( + jtCLIENT, "ShardArchiveHandler", + [this, ptr = shared_from_this(), url, dstDir](Job&) + { + if (!downloader_->download( + url.domain, + std::to_string(url.port.get_value_or(443)), + url.path, + 11, + dstDir / "archive.tar.lz4", + std::bind(&ShardArchiveHandler::complete, + ptr, std::placeholders::_1))) + { + std::lock_guard l(m_); + remove(l); + next(l); + } + }); process_ = true; return true; @@ -206,7 +375,7 @@ ShardArchiveHandler::complete(path dstPath) jtCLIENT, "ShardArchiveHandler", [=, dstPath = std::move(dstPath), ptr = shared_from_this()](Job&) { - // If validating and not synced then defer and retry + // If not synced then defer and retry auto const mode {ptr->app_.getOPs().getOperatingMode()}; if (mode != OperatingMode::FULL) { @@ -282,6 +451,11 @@ ShardArchiveHandler::remove(std::lock_guard&) app_.getShardStore()->removePreShard(shardIndex); archives_.erase(shardIndex); + auto& session{sqliteDB_->getSession()}; + + session << "DELETE FROM State WHERE ShardIndex = :index;", + soci::use(shardIndex); + auto const dstDir {downloadDir_ / std::to_string(shardIndex)}; try { @@ -294,5 +468,37 @@ ShardArchiveHandler::remove(std::lock_guard&) } } +void +ShardArchiveHandler::doRelease(std::lock_guard const&) +{ + process_ = false; + + timer_.cancel(); + for (auto const& ar : archives_) + app_.getShardStore()->removePreShard(ar.first); + archives_.clear(); + + { + auto& session{sqliteDB_->getSession()}; + + session << "DROP TABLE State;"; + } + + sqliteDB_.reset(); + + // Remove temp root download directory + try + { + remove_all(downloadDir_); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << "exception: " << e.what() + << " in function: " << __func__; + } + + downloader_.reset(); +} + } // RPC } // ripple diff --git a/src/test/net/SSLHTTPDownloader_test.cpp b/src/test/net/SSLHTTPDownloader_test.cpp index f8421e7d248..04726137aac 100644 --- a/src/test/net/SSLHTTPDownloader_test.cpp +++ b/src/test/net/SSLHTTPDownloader_test.cpp @@ -17,11 +17,10 @@ */ //============================================================================== -#include +#include #include #include #include -#include #include #include #include @@ -82,15 +81,15 @@ class SSLHTTPDownloader_test : public beast::unit_test::suite beast::Journal journal_; // The SSLHTTPDownloader must be created as shared_ptr // because it uses shared_from_this - std::shared_ptr ptr_; + std::shared_ptr ptr_; Downloader(jtx::Env& env) : journal_ {sink_} - , ptr_ {std::make_shared( + , ptr_ {std::make_shared( env.app().getIOService(), journal_, env.app().config())} {} - SSLHTTPDownloader* operator->() + DatabaseDownloader* operator->() { return ptr_.get(); } @@ -104,6 +103,7 @@ class SSLHTTPDownloader_test : public beast::unit_test::suite (verify ? "Verify" : "No Verify"); using namespace jtx; + ripple::test::detail::FileDirGuard cert { *this, "_cert", "ca.pem", TrustedPublisherServer::ca_cert()}; @@ -152,22 +152,11 @@ class SSLHTTPDownloader_test : public beast::unit_test::suite testFailures() { testcase("Error conditions"); + using namespace jtx; + Env env {*this}; - { - // file exists - Downloader dl {env}; - ripple::test::detail::FileDirGuard const datafile { - *this, "downloads", "data", "file contents"}; - BEAST_EXPECT(!dl->download( - "localhost", - "443", - "", - 11, - datafile.file(), - std::function {std::ref(cb)})); - } { // bad hostname boost::system::error_code ec; diff --git a/src/test/rpc/ShardArchiveHandler_test.cpp b/src/test/rpc/ShardArchiveHandler_test.cpp new file mode 100644 index 00000000000..3515810eabd --- /dev/null +++ b/src/test/rpc/ShardArchiveHandler_test.cpp @@ -0,0 +1,364 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class ShardArchiveHandler_test : public beast::unit_test::suite +{ + using Downloads = std::vector>; + + TrustedPublisherServer + createServer(jtx::Env& env, bool ssl = true) + { + std::vector list; + list.push_back(TrustedPublisherServer::randomValidator()); + return TrustedPublisherServer{ + env.app().getIOService(), + list, + env.timeKeeper().now() + std::chrono::seconds{3600}, + ssl}; + } + +public: + void + testStateDatabase1() + { + testcase("testStateDatabase1"); + + { + beast::temp_dir tempDir; + + auto c = jtx::envconfig(); + auto& section = c->section(ConfigSection::shardDatabase()); + section.set("path", tempDir.path()); + section.set("max_size_gb", "100"); + c->setupControl(true, true, true); + + jtx::Env env(*this, std::move(c)); + auto handler = RPC::ShardArchiveHandler::getInstance( + env.app(), env.app().getJobQueue()); + BEAST_EXPECT(handler); + + BEAST_EXPECT(handler->init()); + + std::string const rawUrl = "https://foo:443/1.tar.lz4"; + parsedURL url; + + parseUrl(url, rawUrl); + handler->add(1, {url, rawUrl}); + + { + std::lock_guard lock(handler->m_); + + auto& session{handler->sqliteDB_->getSession()}; + + soci::rowset rs = + (session.prepare << "SELECT * FROM State;"); + + uint64_t rowCount = 0; + + for (auto it = rs.begin(); it != rs.end(); ++it, ++rowCount) + { + BEAST_EXPECT(it->get(0) == 1); + BEAST_EXPECT(it->get(1) == rawUrl); + } + + BEAST_EXPECT(rowCount == 1); + } + + handler->release(); + } + + // Destroy the singleton so we start fresh in + // the next testcase. + RPC::ShardArchiveHandler::instance_.reset(); + } + + void + testStateDatabase2() + { + testcase("testStateDatabase2"); + + { + beast::temp_dir tempDir; + + auto c = jtx::envconfig(); + auto& section = c->section(ConfigSection::shardDatabase()); + section.set("path", tempDir.path()); + section.set("max_size_gb", "100"); + c->setupControl(true, true, true); + + jtx::Env env(*this, std::move(c)); + auto handler = RPC::ShardArchiveHandler::getInstance( + env.app(), env.app().getJobQueue()); + BEAST_EXPECT(handler); + + BEAST_EXPECT(handler->init()); + + Downloads const dl = {{1, "https://foo:443/1.tar.lz4"}, + {2, "https://foo:443/2.tar.lz4"}, + {3, "https://foo:443/3.tar.lz4"}}; + + for (auto const& entry : dl) + { + parsedURL url; + parseUrl(url, entry.second); + handler->add(entry.first, {url, entry.second}); + } + + { + std::lock_guard lock(handler->m_); + + auto& session{handler->sqliteDB_->getSession()}; + soci::rowset rs = + (session.prepare << "SELECT * FROM State;"); + + uint64_t pos = 0; + for (auto it = rs.begin(); it != rs.end(); ++it, ++pos) + { + BEAST_EXPECT(it->get(0) == dl[pos].first); + BEAST_EXPECT(it->get(1) == dl[pos].second); + } + + BEAST_EXPECT(pos == dl.size()); + } + + handler->release(); + } + + // Destroy the singleton so we start fresh in + // the next testcase. + RPC::ShardArchiveHandler::instance_.reset(); + } + + void + testStateDatabase3() + { + testcase("testStateDatabase3"); + + { + beast::temp_dir tempDir; + + auto c = jtx::envconfig(); + auto& section = c->section(ConfigSection::shardDatabase()); + section.set("path", tempDir.path()); + section.set("max_size_gb", "100"); + c->setupControl(true, true, true); + + jtx::Env env(*this, std::move(c)); + auto handler = RPC::ShardArchiveHandler::getInstance( + env.app(), env.app().getJobQueue()); + BEAST_EXPECT(handler); + + BEAST_EXPECT(handler->init()); + + auto server = createServer(env); + auto host = server.local_endpoint().address().to_string(); + auto port = std::to_string(server.local_endpoint().port()); + server.stop(); + + Downloads const dl = [&host, &port] { + Downloads ret; + + for (int i = 1; i <= 10; ++i) + { + ret.push_back({i, + (boost::format("https://%s:%d/%d.tar.lz4") % + host % port % i) + .str()}); + } + + return ret; + }(); + + for (auto const& entry : dl) + { + parsedURL url; + parseUrl(url, entry.second); + handler->add(entry.first, {url, entry.second}); + } + + BEAST_EXPECT(handler->start()); + + auto stateDir = RPC::ShardArchiveHandler::getDownloadDirectory( + env.app().config()); + + std::unique_lock lock(handler->m_); + + BEAST_EXPECT( + boost::filesystem::exists(stateDir) || + handler->archives_.empty()); + + while (!handler->archives_.empty()) + { + lock.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + lock.lock(); + } + + BEAST_EXPECT(!boost::filesystem::exists(stateDir)); + } + + // Destroy the singleton so we start fresh in + // the next testcase. + RPC::ShardArchiveHandler::instance_.reset(); + } + + void + testStateDatabase4() + { + testcase("testStateDatabase4"); + + beast::temp_dir tempDir; + + { + auto c = jtx::envconfig(); + auto& section = c->section(ConfigSection::shardDatabase()); + section.set("path", tempDir.path()); + section.set("max_size_gb", "100"); + c->setupControl(true, true, true); + + jtx::Env env(*this, std::move(c)); + auto handler = RPC::ShardArchiveHandler::getInstance( + env.app(), env.app().getJobQueue()); + BEAST_EXPECT(handler); + + BEAST_EXPECT(handler->init()); + + auto server = createServer(env); + auto host = server.local_endpoint().address().to_string(); + auto port = std::to_string(server.local_endpoint().port()); + server.stop(); + + Downloads const dl = [&host, &port] { + Downloads ret; + + for (int i = 1; i <= 10; ++i) + { + ret.push_back({i, + (boost::format("https://%s:%d/%d.tar.lz4") % + host % port % i) + .str()}); + } + + return ret; + }(); + + for (auto const& entry : dl) + { + parsedURL url; + parseUrl(url, entry.second); + handler->add(entry.first, {url, entry.second}); + } + + auto stateDir = RPC::ShardArchiveHandler::getDownloadDirectory( + env.app().config()); + + boost::filesystem::copy_file( + stateDir / stateDBName, + boost::filesystem::path(tempDir.path()) / stateDBName); + + BEAST_EXPECT(handler->start()); + + std::unique_lock lock(handler->m_); + + BEAST_EXPECT( + boost::filesystem::exists(stateDir) || + handler->archives_.empty()); + + while (!handler->archives_.empty()) + { + lock.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + lock.lock(); + } + + BEAST_EXPECT(!boost::filesystem::exists(stateDir)); + + boost::filesystem::create_directory(stateDir); + + boost::filesystem::copy_file( + boost::filesystem::path(tempDir.path()) / stateDBName, + stateDir / stateDBName); + } + + // Destroy the singleton so we start fresh in + // the new scope. + RPC::ShardArchiveHandler::instance_.reset(); + + auto c = jtx::envconfig(); + auto& section = c->section(ConfigSection::shardDatabase()); + section.set("path", tempDir.path()); + section.set("max_size_gb", "100"); + c->setupControl(true, true, true); + + jtx::Env env(*this, std::move(c)); + + while (!RPC::ShardArchiveHandler::hasInstance()) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + BEAST_EXPECT(RPC::ShardArchiveHandler::hasInstance()); + + auto handler = RPC::ShardArchiveHandler::getInstance(); + + auto stateDir = + RPC::ShardArchiveHandler::getDownloadDirectory(env.app().config()); + + std::unique_lock lock(handler->m_); + + BEAST_EXPECT( + boost::filesystem::exists(stateDir) || handler->archives_.empty()); + + while (!handler->archives_.empty()) + { + lock.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + lock.lock(); + } + + BEAST_EXPECT(!boost::filesystem::exists(stateDir)); + } + + void + run() override + { + testStateDatabase1(); + testStateDatabase2(); + testStateDatabase3(); + testStateDatabase4(); + } +}; + +BEAST_DEFINE_TESTSUITE(ShardArchiveHandler, app, ripple); + +} // namespace test +} // namespace ripple From d097819c528da21791388ba80a8cf399adf38eb3 Mon Sep 17 00:00:00 2001 From: seelabs Date: Mon, 6 Jan 2020 11:55:09 -0800 Subject: [PATCH 03/14] Check XRP endpoints for circular paths (RIPD-1781): The payment engine restricts payment paths so two steps do not input the same Currency/Issuer or output the same Currency/Issuer. This check was skipped when the path started or ended with XRP. An example of a path that was incorrectly accepted was: XRP -> //USD -> //XRP -> EUR This patch enables the path loop check for paths that start or end with XRP. --- src/ripple/app/paths/impl/XRPEndpointStep.cpp | 13 +++ src/ripple/protocol/Feature.h | 2 + src/ripple/protocol/impl/Feature.cpp | 2 + src/test/app/Flow_test.cpp | 97 ++++++++++++++++++- src/test/app/PayStrand_test.cpp | 2 +- 5 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/ripple/app/paths/impl/XRPEndpointStep.cpp b/src/ripple/app/paths/impl/XRPEndpointStep.cpp index 2c6b91680fe..d30bd1887f7 100644 --- a/src/ripple/app/paths/impl/XRPEndpointStep.cpp +++ b/src/ripple/app/paths/impl/XRPEndpointStep.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -359,6 +360,18 @@ XRPEndpointStep::check (StrandContext const& ctx) const if (ter != tesSUCCESS) return ter; + if (ctx.view.rules().enabled(fix1781)) + { + auto const issuesIndex = isLast_ ? 0 : 1; + if (!ctx.seenDirectIssues[issuesIndex].insert(xrpIssue()).second) + { + JLOG(j_.debug()) + << "XRPEndpointStep: loop detected: Index: " << ctx.strandSize + << ' ' << *this; + return temBAD_PATH_LOOP; + } + } + return tesSUCCESS; } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d11a7e3e649..3a7ee97862d 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -111,6 +111,7 @@ class FeatureCollections // fixQualityUpperBound should be activated before FlowCross "fixQualityUpperBound", "RequireFullyCanonicalSig", + "fix1781", // XRPEndpointSteps should be included in the circular payment check }; std::vector features; @@ -399,6 +400,7 @@ extern uint256 const fixPayChanRecipientOwnerDir; extern uint256 const featureDeletableAccounts; extern uint256 const fixQualityUpperBound; extern uint256 const featureRequireFullyCanonicalSig; +extern uint256 const fix1781; } // ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index db96583a058..bac6e2e8f33 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -130,6 +130,7 @@ detail::supportedAmendments () "DeletableAccounts", "fixQualityUpperBound", "RequireFullyCanonicalSig", + "fix1781", }; return supported; } @@ -189,5 +190,6 @@ uint256 const fixPayChanRecipientOwnerDir = *getRegisteredFeature("fixPayChanRec uint256 const featureDeletableAccounts = *getRegisteredFeature("DeletableAccounts"); uint256 const fixQualityUpperBound = *getRegisteredFeature("fixQualityUpperBound"); uint256 const featureRequireFullyCanonicalSig = *getRegisteredFeature("RequireFullyCanonicalSig"); +uint256 const fix1781 = *getRegisteredFeature("fix1781"); } // ripple diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 9a7637fa471..bfd1460b4d4 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1180,6 +1180,100 @@ struct Flow_test : public beast::unit_test::suite ter(temBAD_PATH)); } + void + testXRPPathLoop() + { + testcase("Circular XRP"); + + using namespace jtx; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + + for (auto const withFix : {true, false}) + { + auto const feats = [&withFix]() -> FeatureBitset { + if (withFix) + return supported_amendments(); + return supported_amendments() - FeatureBitset{fix1781}; + }(); + { + // Payment path starting with XRP + Env env(*this, feats); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, alice, EUR(100))); + env.close(); + + env(offer(alice, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(alice, USD(100), XRP(100)), txflags(tfPassive)); + env(offer(alice, XRP(100), EUR(100)), txflags(tfPassive)); + env.close(); + + TER const expectedTer = + withFix ? TER{temBAD_PATH_LOOP} : TER{tesSUCCESS}; + env(pay(alice, bob, EUR(1)), + path(~USD, ~XRP, ~EUR), + sendmax(XRP(1)), + txflags(tfNoRippleDirect), + ter(expectedTer)); + } + pass(); + } + { + // Payment path ending with XRP + Env env(*this); + env.fund(XRP(10000), alice, bob, gw); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, alice, EUR(100))); + env.close(); + + env(offer(alice, XRP(100), USD(100)), txflags(tfPassive)); + env(offer(alice, EUR(100), XRP(100)), txflags(tfPassive)); + env.close(); + // EUR -> //XRP -> //USD ->XRP + env(pay(alice, bob, XRP(1)), + path(~XRP, ~USD, ~XRP), + sendmax(EUR(1)), + txflags(tfNoRippleDirect), + ter(temBAD_PATH_LOOP)); + } + { + // Payment where loop is formed in the middle of the path, not on an + // endpoint + auto const JPY = gw["JPY"]; + Env env(*this); + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env.trust(EUR(1000), alice, bob); + env.trust(JPY(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(100))); + env(pay(gw, alice, EUR(100))); + env(pay(gw, alice, JPY(100))); + env.close(); + + env(offer(alice, USD(100), XRP(100)), txflags(tfPassive)); + env(offer(alice, XRP(100), EUR(100)), txflags(tfPassive)); + env(offer(alice, EUR(100), XRP(100)), txflags(tfPassive)); + env(offer(alice, XRP(100), JPY(100)), txflags(tfPassive)); + env.close(); + + env(pay(alice, bob, JPY(1)), + path(~XRP, ~EUR, ~XRP, ~JPY), + sendmax(USD(1)), + txflags(tfNoRippleDirect), + ter(temBAD_PATH_LOOP)); + } + } + void testWithFeats(FeatureBitset features) { using namespace jtx; @@ -1204,6 +1298,7 @@ struct Flow_test : public beast::unit_test::suite void run() override { testLimitQuality(); + testXRPPathLoop(); testRIPD1443(); testRIPD1449(); @@ -1231,7 +1326,7 @@ struct Flow_manual_test : public Flow_test testWithFeats(all - flowCross - f1513); testWithFeats(all - flowCross ); testWithFeats(all - f1513); - testWithFeats(all ); + testWithFeats(all ); testEmptyStrand(all - f1513); testEmptyStrand(all ); diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 3b7c4c6f9a4..59f8967e136 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -703,7 +703,7 @@ struct PayStrand_test : public beast::unit_test::suite alice, /*deliver*/ xrpIssue(), /*limitQuality*/ boost::none, - /*sendMaxIssue*/ xrpIssue(), + /*sendMaxIssue*/ EUR.issue(), path, true, false, From ade5eb71cf7e2d0316dfe2f1723206bd152915ca Mon Sep 17 00:00:00 2001 From: seelabs Date: Tue, 24 Mar 2020 22:25:41 -0700 Subject: [PATCH 04/14] Fix unneeded copies in range some range for loops: clang 10 warns about an unneeded copy for these range for loops (range-loop-construct warnings) --- src/test/app/PayChan_test.cpp | 6 +++--- src/test/rpc/AccountCurrencies_test.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 22764086da1..5634ac54a69 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -946,7 +946,7 @@ struct PayChan_test : public beast::unit_test::suite Env env(*this); env.fund(XRP(10000), alice); - for (auto const a : bobs) + for (auto const& a : bobs) { env.fund(XRP(10000), a); env.close(); @@ -956,7 +956,7 @@ struct PayChan_test : public beast::unit_test::suite // create a channel from alice to every bob account auto const settleDelay = 3600s; auto const channelFunds = XRP(1); - for (auto const b : bobs) + for (auto const& b : bobs) { env(create(alice, b, channelFunds, settleDelay, alice.pk())); } @@ -990,7 +990,7 @@ struct PayChan_test : public beast::unit_test::suite auto const bobsB58 = [&bobs]() -> std::set { std::set r; - for (auto const a : bobs) + for (auto const& a : bobs) r.insert(a.human()); return r; }(); diff --git a/src/test/rpc/AccountCurrencies_test.cpp b/src/test/rpc/AccountCurrencies_test.cpp index 85469d33106..5825d992f40 100644 --- a/src/test/rpc/AccountCurrencies_test.cpp +++ b/src/test/rpc/AccountCurrencies_test.cpp @@ -153,7 +153,7 @@ class AccountCurrencies_test : public beast::unit_test::suite // does not change env(trust(alice, gw["USD"](100), tfSetFreeze)); result = env.rpc ("account_lines", alice.human()); - for (auto const l : result[jss::lines]) + for (auto const& l : result[jss::lines]) BEAST_EXPECT( l[jss::freeze].asBool() == (l[jss::currency] == "USD")); result = env.rpc ("json", "account_currencies", From 758a3792ebcd877a42f87f8f81a4d14fd68aad68 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sat, 15 Feb 2020 10:50:52 -0500 Subject: [PATCH 05/14] Add protocol message compression support: * Peers negotiate compression via HTTP Header "X-Offer-Compression: lz4" * Messages greater than 70 bytes and protocol type messages MANIFESTS, ENDPOINTS, TRANSACTION, GET_LEDGER, LEDGER_DATA, GET_OBJECT, and VALIDATORLIST are compressed * If the compressed message is larger than the uncompressed message then the uncompressed message is sent * Compression flag and the compression algorithm type are included in the message header * Only LZ4 block compression is currently supported --- Builds/CMake/RippledCore.cmake | 1 + src/ripple/basics/CompressionAlgorithms.h | 153 +++++++++ src/ripple/core/Config.h | 3 + src/ripple/core/ConfigSections.h | 1 + src/ripple/core/impl/Config.cpp | 3 + src/ripple/overlay/Compression.h | 103 ++++++ src/ripple/overlay/Message.h | 53 ++- src/ripple/overlay/impl/ConnectAttempt.cpp | 6 +- src/ripple/overlay/impl/ConnectAttempt.h | 2 +- src/ripple/overlay/impl/Message.cpp | 134 +++++++- src/ripple/overlay/impl/PeerImp.cpp | 9 +- src/ripple/overlay/impl/PeerImp.h | 6 + src/ripple/overlay/impl/ProtocolMessage.h | 58 +++- src/test/overlay/compression_test.cpp | 376 +++++++++++++++++++++ 14 files changed, 873 insertions(+), 35 deletions(-) create mode 100644 src/ripple/basics/CompressionAlgorithms.h create mode 100644 src/ripple/overlay/Compression.h create mode 100644 src/test/overlay/compression_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index ad9925b10ba..db56e1cf287 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -855,6 +855,7 @@ target_sources (rippled PRIVATE src/test/overlay/ProtocolVersion_test.cpp src/test/overlay/cluster_test.cpp src/test/overlay/short_read_test.cpp + src/test/overlay/compression_test.cpp #[===============================[ test sources: subdir: peerfinder diff --git a/src/ripple/basics/CompressionAlgorithms.h b/src/ripple/basics/CompressionAlgorithms.h new file mode 100644 index 00000000000..3cd67c753d8 --- /dev/null +++ b/src/ripple/basics/CompressionAlgorithms.h @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLED_COMPRESSIONALGORITHMS_H_INCLUDED +#define RIPPLED_COMPRESSIONALGORITHMS_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +namespace compression_algorithms { + +/** Convenience wrapper for Throw + * @param message Message to log/throw + */ +inline void doThrow(const char *message) +{ + Throw(message); +} + +/** LZ4 block compression. + * @tparam BufferFactory Callable object or lambda. + * Takes the requested buffer size and returns allocated buffer pointer. + * @param in Data to compress + * @param inSize Size of the data + * @param bf Compressed buffer allocator + * @return Size of compressed data, or zero if failed to compress + */ +template +std::size_t +lz4Compress(void const* in, + std::size_t inSize, BufferFactory&& bf) +{ + if (inSize > UINT32_MAX) + doThrow("lz4 compress: invalid size"); + + auto const outCapacity = LZ4_compressBound(inSize); + + // Request the caller to allocate and return the buffer to hold compressed data + auto compressed = bf(outCapacity); + + auto compressedSize = LZ4_compress_default( + reinterpret_cast(in), + reinterpret_cast(compressed), + inSize, + outCapacity); + if (compressedSize == 0) + doThrow("lz4 compress: failed"); + + return compressedSize; +} + +/** + * @param in Compressed data + * @param inSize Size of compressed data + * @param decompressed Buffer to hold decompressed data + * @param decompressedSize Size of the decompressed buffer + * @return size of the decompressed data + */ +inline +std::size_t +lz4Decompress(std::uint8_t const* in, std::size_t inSize, + std::uint8_t* decompressed, std::size_t decompressedSize) +{ + auto ret = LZ4_decompress_safe(reinterpret_cast(in), + reinterpret_cast(decompressed), inSize, decompressedSize); + + if (ret <= 0 || ret != decompressedSize) + doThrow("lz4 decompress: failed"); + + return decompressedSize; +} + +/** LZ4 block decompression. + * @tparam InputStream ZeroCopyInputStream + * @param in Input source stream + * @param inSize Size of compressed data + * @param decompressed Buffer to hold decompressed data + * @param decompressedSize Size of the decompressed buffer + * @return size of the decompressed data + */ +template +std::size_t +lz4Decompress(InputStream& in, std::size_t inSize, + std::uint8_t* decompressed, std::size_t decompressedSize) +{ + std::vector compressed; + std::uint8_t const* chunk = nullptr; + int chunkSize = 0; + int copiedInSize = 0; + auto const currentBytes = in.ByteCount(); + + // Use the first chunk if it is >= inSize bytes of the compressed message. + // Otherwise copy inSize bytes of chunks into compressed buffer and + // use the buffer to decompress. + while (in.Next(reinterpret_cast(&chunk), &chunkSize)) + { + if (copiedInSize == 0) + { + if (chunkSize >= inSize) + { + copiedInSize = inSize; + break; + } + compressed.resize(inSize); + } + + chunkSize = chunkSize < (inSize - copiedInSize) ? chunkSize : (inSize - copiedInSize); + + std::copy(chunk, chunk + chunkSize, compressed.data() + copiedInSize); + + copiedInSize += chunkSize; + + if (copiedInSize == inSize) + { + chunk = compressed.data(); + break; + } + } + + // Put back unused bytes + if (in.ByteCount() > (currentBytes + copiedInSize)) + in.BackUp(in.ByteCount() - currentBytes - copiedInSize); + + if ((copiedInSize == 0 && chunkSize < inSize) || (copiedInSize > 0 && copiedInSize != inSize)) + doThrow("lz4 decompress: insufficient input size"); + + return lz4Decompress(chunk, inSize, decompressed, decompressedSize); +} + +} // compression + +} // ripple + +#endif //RIPPLED_COMPRESSIONALGORITHMS_H_INCLUDED diff --git a/src/ripple/core/Config.h b/src/ripple/core/Config.h index e1200c63cef..5231f169468 100644 --- a/src/ripple/core/Config.h +++ b/src/ripple/core/Config.h @@ -171,6 +171,9 @@ class Config : public BasicConfig std::string SSL_VERIFY_FILE; std::string SSL_VERIFY_DIR; + // Compression + bool COMPRESSION = false; + // Thread pool configuration std::size_t WORKERS = 0; diff --git a/src/ripple/core/ConfigSections.h b/src/ripple/core/ConfigSections.h index e5f1a3f490e..653b9c404e4 100644 --- a/src/ripple/core/ConfigSections.h +++ b/src/ripple/core/ConfigSections.h @@ -37,6 +37,7 @@ struct ConfigSection // VFALCO TODO Rename and replace these macros with variables. #define SECTION_AMENDMENTS "amendments" #define SECTION_CLUSTER_NODES "cluster_nodes" +#define SECTION_COMPRESSION "compression" #define SECTION_DEBUG_LOGFILE "debug_logfile" #define SECTION_ELB_SUPPORT "elb_support" #define SECTION_FEE_DEFAULT "fee_default" diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index b49bcebffb9..36732800616 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -454,6 +454,9 @@ void Config::loadFromString (std::string const& fileContents) if (getSingleSection (secConfig, SECTION_WORKERS, strTemp, j_)) WORKERS = beast::lexicalCastThrow (strTemp); + if (getSingleSection (secConfig, SECTION_COMPRESSION, strTemp, j_)) + COMPRESSION = beast::lexicalCastThrow (strTemp); + // Do not load trusted validator configuration for standalone mode if (! RUN_STANDALONE) { diff --git a/src/ripple/overlay/Compression.h b/src/ripple/overlay/Compression.h new file mode 100644 index 00000000000..efec6b524e0 --- /dev/null +++ b/src/ripple/overlay/Compression.h @@ -0,0 +1,103 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLED_COMPRESSION_H_INCLUDED +#define RIPPLED_COMPRESSION_H_INCLUDED + +#include +#include +#include + +namespace ripple { + +namespace compression { + +std::size_t constexpr headerBytes = 6; +std::size_t constexpr headerBytesCompressed = 10; + +enum class Algorithm : std::uint8_t { + None = 0x00, + LZ4 = 0x01 +}; + +enum class Compressed : std::uint8_t { + On, + Off +}; + +/** Decompress input stream. + * @tparam InputStream ZeroCopyInputStream + * @param in Input source stream + * @param inSize Size of compressed data + * @param decompressed Buffer to hold decompressed message + * @param algorithm Compression algorithm type + * @return Size of decompressed data or zero if failed to decompress + */ +template +std::size_t +decompress(InputStream& in, std::size_t inSize, std::uint8_t* decompressed, + std::size_t decompressedSize, Algorithm algorithm = Algorithm::LZ4) { + try + { + if (algorithm == Algorithm::LZ4) + return ripple::compression_algorithms::lz4Decompress(in, inSize, + decompressed, decompressedSize); + else + { + JLOG(debugLog().warn()) << "decompress: invalid compression algorithm " + << static_cast(algorithm); + assert(0); + } + } + catch (...) {} + return 0; +} + +/** Compress input data. + * @tparam BufferFactory Callable object or lambda. + * Takes the requested buffer size and returns allocated buffer pointer. + * @param in Data to compress + * @param inSize Size of the data + * @param bf Compressed buffer allocator + * @param algorithm Compression algorithm type + * @return Size of compressed data, or zero if failed to compress + */ +template +std::size_t +compress(void const* in, + std::size_t inSize, BufferFactory&& bf, Algorithm algorithm = Algorithm::LZ4) { + try + { + if (algorithm == Algorithm::LZ4) + return ripple::compression_algorithms::lz4Compress(in, inSize, std::forward(bf)); + else + { + JLOG(debugLog().warn()) << "compress: invalid compression algorithm" + << static_cast(algorithm); + assert(0); + } + } + catch (...) {} + return 0; +} +} // compression + +} // ripple + +#endif //RIPPLED_COMPRESSION_H_INCLUDED diff --git a/src/ripple/overlay/Message.h b/src/ripple/overlay/Message.h index e186272b3bf..b3ae036a500 100644 --- a/src/ripple/overlay/Message.h +++ b/src/ripple/overlay/Message.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_OVERLAY_MESSAGE_H_INCLUDED #define RIPPLE_OVERLAY_MESSAGE_H_INCLUDED +#include #include #include #include @@ -47,27 +48,61 @@ namespace ripple { class Message : public std::enable_shared_from_this { + using Compressed = compression::Compressed; + using Algorithm = compression::Algorithm; public: + /** Constructor + * @param message Protocol message to serialize + * @param type Protocol message type + */ Message (::google::protobuf::Message const& message, int type); -public: - /** Retrieve the packed message data. */ + /** Retrieve the packed message data. If compressed message is requested but the message + * is not compressible then the uncompressed buffer is returned. + * @param compressed Request compressed (Compress::On) or + * uncompressed (Compress::Off) payload buffer + * @return Payload buffer + */ std::vector const& - getBuffer () const - { - return mBuffer; - } + getBuffer (Compressed tryCompressed); /** Get the traffic category */ std::size_t getCategory () const { - return mCategory; + return category_; } private: - std::vector mBuffer; - std::size_t mCategory; + std::vector buffer_; + std::vector bufferCompressed_; + std::size_t category_; + std::once_flag once_flag_; + + /** Set the payload header + * @param in Pointer to the payload + * @param payloadBytes Size of the payload excluding the header size + * @param type Protocol message type + * @param comprAlgorithm Compression algorithm used in compression, + * currently LZ4 only. If None then the message is uncompressed. + * @param uncompressedBytes Size of the uncompressed message + */ + void setHeader(std::uint8_t* in, std::uint32_t payloadBytes, int type, + Algorithm comprAlgorithm, std::uint32_t uncompressedBytes); + + /** Try to compress the payload. + * Can be called concurrently by multiple peers but is compressed once. + * If the message is not compressible then the serialized buffer_ is used. + */ + void compress(); + + /** Get the message type from the payload header. + * First four bytes are the compression/algorithm flag and the payload size. + * Next two bytes are the message type + * @param in Payload header pointer + * @return Message type + */ + int getType(std::uint8_t const* in) const; }; } diff --git a/src/ripple/overlay/impl/ConnectAttempt.cpp b/src/ripple/overlay/impl/ConnectAttempt.cpp index 2c79e7ccc24..4cd38715fcc 100644 --- a/src/ripple/overlay/impl/ConnectAttempt.cpp +++ b/src/ripple/overlay/impl/ConnectAttempt.cpp @@ -197,7 +197,7 @@ ConnectAttempt::onHandshake (error_code ec) if (! sharedValue) return close(); // makeSharedValue logs - req_ = makeRequest(!overlay_.peerFinder().config().peerPrivate); + req_ = makeRequest(!overlay_.peerFinder().config().peerPrivate, app_.config().COMPRESSION); buildHandshake(req_, *sharedValue, overlay_.setup().networkID, overlay_.setup().public_ip, remote_endpoint_.address(), app_); @@ -264,7 +264,7 @@ ConnectAttempt::onShutdown (error_code ec) //-------------------------------------------------------------------------- auto -ConnectAttempt::makeRequest (bool crawl) -> request_type +ConnectAttempt::makeRequest (bool crawl, bool compressionEnabled) -> request_type { request_type m; m.method(boost::beast::http::verb::get); @@ -275,6 +275,8 @@ ConnectAttempt::makeRequest (bool crawl) -> request_type m.insert ("Connection", "Upgrade"); m.insert ("Connect-As", "Peer"); m.insert ("Crawl", crawl ? "public" : "private"); + if (compressionEnabled) + m.insert("X-Offer-Compression", "lz4"); return m; } diff --git a/src/ripple/overlay/impl/ConnectAttempt.h b/src/ripple/overlay/impl/ConnectAttempt.h index 601207de13d..5464ae8cdfd 100644 --- a/src/ripple/overlay/impl/ConnectAttempt.h +++ b/src/ripple/overlay/impl/ConnectAttempt.h @@ -93,7 +93,7 @@ class ConnectAttempt static request_type - makeRequest (bool crawl); + makeRequest (bool crawl, bool compressionEnabled); void processResponse(); diff --git a/src/ripple/overlay/impl/Message.cpp b/src/ripple/overlay/impl/Message.cpp index acf201e49e9..edac6b41248 100644 --- a/src/ripple/overlay/impl/Message.cpp +++ b/src/ripple/overlay/impl/Message.cpp @@ -17,7 +17,6 @@ */ //============================================================================== -#include #include #include #include @@ -25,8 +24,9 @@ namespace ripple { Message::Message (::google::protobuf::Message const& message, int type) - : mCategory(TrafficCount::categorize(message, type, false)) + : category_(TrafficCount::categorize(message, type, false)) { + using namespace ripple::compression; #if defined(GOOGLE_PROTOBUF_VERSION) && (GOOGLE_PROTOBUF_VERSION >= 3011000) auto const messageBytes = message.ByteSizeLong (); @@ -36,23 +36,129 @@ Message::Message (::google::protobuf::Message const& message, int type) assert (messageBytes != 0); - /** Number of bytes in a message header. */ - std::size_t constexpr headerBytes = 6; + buffer_.resize (headerBytes + messageBytes); - mBuffer.resize (headerBytes + messageBytes); + setHeader(buffer_.data(), messageBytes, type, Algorithm::None, 0); - auto ptr = mBuffer.data(); + if (messageBytes != 0) + message.SerializeToArray(buffer_.data() + headerBytes, messageBytes); +} + +void +Message::compress() +{ + using namespace ripple::compression; + auto const messageBytes = buffer_.size () - headerBytes; - *ptr++ = static_cast((messageBytes >> 24) & 0xFF); - *ptr++ = static_cast((messageBytes >> 16) & 0xFF); - *ptr++ = static_cast((messageBytes >> 8) & 0xFF); - *ptr++ = static_cast(messageBytes & 0xFF); + auto type = getType(buffer_.data()); - *ptr++ = static_cast((type >> 8) & 0xFF); - *ptr++ = static_cast (type & 0xFF); + bool const compressible = [&]{ + if (messageBytes <= 70) + return false; + switch(type) + { + case protocol::mtMANIFESTS: + case protocol::mtENDPOINTS: + case protocol::mtTRANSACTION: + case protocol::mtGET_LEDGER: + case protocol::mtLEDGER_DATA: + case protocol::mtGET_OBJECTS: + case protocol::mtVALIDATORLIST: + return true; + case protocol::mtPING: + case protocol::mtCLUSTER: + case protocol::mtPROPOSE_LEDGER: + case protocol::mtSTATUS_CHANGE: + case protocol::mtHAVE_SET: + case protocol::mtVALIDATION: + case protocol::mtGET_SHARD_INFO: + case protocol::mtSHARD_INFO: + case protocol::mtGET_PEER_SHARD_INFO: + case protocol::mtPEER_SHARD_INFO: + break; + } + return false; + }(); - if (messageBytes != 0) - message.SerializeToArray(ptr, messageBytes); + if (compressible) + { + auto payload = static_cast(buffer_.data() + headerBytes); + + auto compressedSize = ripple::compression::compress( + payload, + messageBytes, + [&](std::size_t inSize) { // size of required compressed buffer + bufferCompressed_.resize(inSize + headerBytesCompressed); + return (bufferCompressed_.data() + headerBytesCompressed); + }); + + if (compressedSize < (messageBytes - (headerBytesCompressed - headerBytes))) + { + bufferCompressed_.resize(headerBytesCompressed + compressedSize); + setHeader(bufferCompressed_.data(), compressedSize, type, Algorithm::LZ4, messageBytes); + } + else + bufferCompressed_.resize(0); + } +} + +/** Set payload header + * Uncompressed message header + * 47-42 Set to 0 + * 41-16 Payload size + * 15-0 Message Type + * Compressed message header + * 79 Set to 0, indicates the message is compressed + * 78-76 Compression algorithm, value 1-7. Set to 1 to indicate LZ4 compression + * 75-74 Set to 0 + * 73-48 Payload size + * 47-32 Message Type + * 31-0 Uncompressed message size +*/ +void +Message::setHeader(std::uint8_t* in, std::uint32_t payloadBytes, int type, + Algorithm comprAlgorithm, std::uint32_t uncompressedBytes) +{ + auto h = in; + + auto pack = [](std::uint8_t*& in, std::uint32_t size) { + *in++ = static_cast((size >> 24) & 0x0F); // leftmost 4 are compression bits + *in++ = static_cast((size >> 16) & 0xFF); + *in++ = static_cast((size >> 8) & 0xFF); + *in++ = static_cast(size & 0xFF); + }; + + pack(in, payloadBytes); + + *in++ = static_cast((type >> 8) & 0xFF); + *in++ = static_cast (type & 0xFF); + + if (comprAlgorithm != Algorithm::None) + { + pack(in, uncompressedBytes); + *h |= 0x80 | (static_cast(comprAlgorithm) << 4); + } +} + +std::vector const& +Message::getBuffer (Compressed tryCompressed) +{ + if (tryCompressed == Compressed::Off) + return buffer_; + + std::call_once(once_flag_, &Message::compress, this); + + if (bufferCompressed_.size() > 0) + return bufferCompressed_; + else + return buffer_; +} + +int +Message::getType(std::uint8_t const* in) const +{ + int type = (static_cast(*(in + 4)) << 8) + *(in + 5); + return type; } } diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index 7908914e110..0733bfe99ad 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -86,6 +86,7 @@ PeerImp::PeerImp (Application& app, id_t id, , slot_ (slot) , request_(std::move(request)) , headers_(request_) + , compressionEnabled_(headers_["X-Offer-Compression"] == "lz4" ? Compressed::On : Compressed::Off) { } @@ -219,7 +220,7 @@ PeerImp::send (std::shared_ptr const& m) overlay_.reportTraffic ( safe_cast(m->getCategory()), - false, static_cast(m->getBuffer().size())); + false, static_cast(m->getBuffer(compressionEnabled_).size())); auto sendq_size = send_queue_.size(); @@ -246,7 +247,7 @@ PeerImp::send (std::shared_ptr const& m) boost::asio::async_write( stream_, - boost::asio::buffer(send_queue_.front()->getBuffer()), + boost::asio::buffer(send_queue_.front()->getBuffer(compressionEnabled_)), bind_executor( strand_, std::bind( @@ -757,6 +758,8 @@ PeerImp::makeResponse (bool crawl, resp.insert("Connect-As", "Peer"); resp.insert("Server", BuildInfo::getFullVersionString()); resp.insert("Crawl", crawl ? "public" : "private"); + if (req["X-Offer-Compression"] == "lz4" && app_.config().COMPRESSION) + resp.insert("X-Offer-Compression", "lz4"); buildHandshake(resp, sharedValue, overlay_.setup().networkID, overlay_.setup().public_ip, remote_ip, app_); @@ -945,7 +948,7 @@ PeerImp::onWriteMessage (error_code ec, std::size_t bytes_transferred) // Timeout on writes only return boost::asio::async_write( stream_, - boost::asio::buffer(send_queue_.front()->getBuffer()), + boost::asio::buffer(send_queue_.front()->getBuffer(compressionEnabled_)), bind_executor( strand_, std::bind( diff --git a/src/ripple/overlay/impl/PeerImp.h b/src/ripple/overlay/impl/PeerImp.h index 559acb26bc0..0d484517160 100644 --- a/src/ripple/overlay/impl/PeerImp.h +++ b/src/ripple/overlay/impl/PeerImp.h @@ -99,6 +99,7 @@ class PeerImp using address_type = boost::asio::ip::address; using endpoint_type = boost::asio::ip::tcp::endpoint; using waitable_timer = boost::asio::basic_waitable_timer; + using Compressed = compression::Compressed; Application& app_; id_t const id_; @@ -201,6 +202,8 @@ class PeerImp std::mutex mutable shardInfoMutex_; hash_map shardInfo_; + Compressed compressionEnabled_ = Compressed::Off; + friend class OverlayImpl; class Metrics { @@ -600,6 +603,9 @@ PeerImp::PeerImp (Application& app, std::unique_ptr&& stream_ptr, , slot_ (std::move(slot)) , response_(std::move(response)) , headers_(response_) + , compressionEnabled_( + headers_["X-Offer-Compression"] == "lz4" && app_.config().COMPRESSION + ? Compressed::On : Compressed::Off) { read_buffer_.commit (boost::asio::buffer_copy(read_buffer_.prepare( boost::asio::buffer_size(buffers)), buffers)); diff --git a/src/ripple/overlay/impl/ProtocolMessage.h b/src/ripple/overlay/impl/ProtocolMessage.h index 8bc8ef77bb9..be4a93fbb4a 100644 --- a/src/ripple/overlay/impl/ProtocolMessage.h +++ b/src/ripple/overlay/impl/ProtocolMessage.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -81,36 +82,66 @@ struct MessageHeader /** The size of the payload on the wire. */ std::uint32_t payload_wire_size = 0; + /** Uncompressed message size if the message is compressed. */ + std::uint32_t uncompressed_size = 0; + /** The type of the message. */ std::uint16_t message_type = 0; + + /** Indicates which compression algorithm the payload is compressed with. + * Currenly only lz4 is supported. If None then the message is not compressed. + */ + compression::Algorithm algorithm = compression::Algorithm::None; }; +template +auto +buffersBegin(BufferSequence const &bufs) +{ + return boost::asio::buffers_iterator::begin(bufs); +} + template boost::optional parseMessageHeader( BufferSequence const& bufs, std::size_t size) { - auto iter = boost::asio::buffers_iterator::begin(bufs); + using namespace ripple::compression; + auto iter = buffersBegin(bufs); MessageHeader hdr; + auto const compressed = (*iter & 0x80) == 0x80; - // Version 1 header: uncompressed payload. - // The top six bits of the first byte are 0. - if ((*iter & 0xFC) == 0) + // Check valid header + if ((*iter & 0xFC) == 0 || compressed) { - hdr.header_size = 6; + hdr.header_size = compressed ? headerBytesCompressed : headerBytes; if (size < hdr.header_size) return {}; + if (compressed) + { + uint8_t algorithm = (*iter & 0x70) >> 4; + if (algorithm != static_cast(compression::Algorithm::LZ4)) + return {}; + hdr.algorithm = compression::Algorithm::LZ4; + } + for (int i = 0; i != 4; ++i) hdr.payload_wire_size = (hdr.payload_wire_size << 8) + *iter++; + // clear the compression bits + hdr.payload_wire_size &= 0x03FFFFFF; hdr.total_wire_size = hdr.header_size + hdr.payload_wire_size; for (int i = 0; i != 2; ++i) hdr.message_type = (hdr.message_type << 8) + *iter++; + if (compressed) + for (int i = 0; i != 4; ++i) + hdr.uncompressed_size = (hdr.uncompressed_size << 8) + *iter++; + return hdr; } @@ -130,7 +161,22 @@ invoke ( ZeroCopyInputStream stream(buffers); stream.Skip(header.header_size); - if (! m->ParseFromZeroCopyStream(&stream)) + if (header.algorithm != compression::Algorithm::None) + { + std::vector payload; + payload.resize(header.uncompressed_size); + + auto payloadSize = ripple::compression::decompress( + stream, + header.payload_wire_size, + payload.data(), + header.uncompressed_size, + header.algorithm); + + if (payloadSize == 0 || !m->ParseFromArray(payload.data(), payloadSize)) + return false; + } + else if (!m->ParseFromZeroCopyStream(&stream)) return false; handler.onMessageBegin (header.message_type, m, header.payload_wire_size); diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp new file mode 100644 index 00000000000..6d3ba5a9aa5 --- /dev/null +++ b/src/test/overlay/compression_test.cpp @@ -0,0 +1,376 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#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 ripple { + +namespace test { + +using namespace ripple::test; +using namespace ripple::test::jtx; + +static +uint256 +ledgerHash (LedgerInfo const& info) +{ +return ripple::sha512Half( + HashPrefix::ledgerMaster, + std::uint32_t(info.seq), + std::uint64_t(info.drops.drops ()), + info.parentHash, + info.txHash, + info.accountHash, + std::uint32_t(info.parentCloseTime.time_since_epoch().count()), + std::uint32_t(info.closeTime.time_since_epoch().count()), + std::uint8_t(info.closeTimeResolution.count()), + std::uint8_t(info.closeFlags)); +} + +class compression_test : public beast::unit_test::suite { + using Compressed = compression::Compressed; + using Algorithm = compression::Algorithm; +public: + compression_test() {} + + template + void + doTest(std::shared_ptr proto, protocol::MessageType mt, uint16_t nbuffers, const char *msg, + bool log = false) { + + if (log) + printf("=== compress/decompress %s ===\n", msg); + Message m(*proto, mt); + + auto &buffer = m.getBuffer(Compressed::On); + + if (log) + printf("==> compressed, original %d bytes, compressed %d bytes\n", + (int)m.getBuffer(Compressed::Off).size(), + (int)m.getBuffer(Compressed::On).size()); + + boost::beast::multi_buffer buffers; + + + // simulate multi-buffer + auto sz = buffer.size() / nbuffers; + for (int i = 0; i < nbuffers; i++) { + auto start = buffer.begin() + sz * i; + auto end = i < nbuffers - 1 ? (buffer.begin() + sz * (i + 1)) : buffer.end(); + std::vector slice(start, end); + buffers.commit( + boost::asio::buffer_copy(buffers.prepare(slice.size()), boost::asio::buffer(slice))); + } + auto header = ripple::detail::parseMessageHeader(buffers.data(), buffer.size()); + + if (log) + printf("==> parsed header: buffers size %d, compressed %d, algorithm %d, header size %d, payload size %d, buffer size %d\n", + (int)buffers.size(), header->algorithm != Algorithm::None, (int)header->algorithm, + (int)header->header_size, (int)header->payload_wire_size, (int)buffer.size()); + + if (header->algorithm == Algorithm::None) { + if (log) + printf("==> NOT COMPRESSED\n"); + return; + } + + std::vector decompressed; + decompressed.resize(header->uncompressed_size); + + BEAST_EXPECT(header->payload_wire_size == buffer.size() - header->header_size); + + ZeroCopyInputStream stream(buffers.data()); + stream.Skip(header->header_size); + + auto decompressedSize = ripple::compression::decompress(stream, header->payload_wire_size, + decompressed.data(), header->uncompressed_size); + BEAST_EXPECT(decompressedSize == header->uncompressed_size); + auto const proto1 = std::make_shared(); + + BEAST_EXPECT(proto1->ParseFromArray(decompressed.data(), decompressedSize)); + auto uncompressed = m.getBuffer(Compressed::Off); + BEAST_EXPECT(std::equal(uncompressed.begin() + ripple::compression::headerBytes, + uncompressed.end(), + decompressed.begin())); + if (log) + printf("\n"); + } + + std::shared_ptr + buildManifests(int n) { + auto manifests = std::make_shared(); + manifests->mutable_list()->Reserve(n); + for (int i = 0; i < n; i++) { + auto master = randomKeyPair(KeyType::ed25519); + auto signing = randomKeyPair(KeyType::ed25519); + STObject st(sfGeneric); + st[sfSequence] = i; + st[sfPublicKey] = std::get<0>(master); + st[sfSigningPubKey] = std::get<0>(signing); + st[sfDomain] = makeSlice(std::string("example") + std::to_string(i) + std::string(".com")); + sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(master), sfMasterSignature); + sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing)); + Serializer s; + st.add(s); + auto *manifest = manifests->add_list(); + manifest->set_stobject(s.data(), s.size()); + } + return manifests; + } + + std::shared_ptr + buildEndpoints(int n) { + auto endpoints = std::make_shared(); + endpoints->mutable_endpoints()->Reserve(n); + for (int i = 0; i < n; i++) { + auto *endpoint = endpoints->add_endpoints(); + endpoint->set_hops(i); + std::string addr = std::string("10.0.1.") + std::to_string(i); + endpoint->mutable_ipv4()->set_ipv4( + boost::endian::native_to_big(boost::asio::ip::address_v4::from_string(addr).to_uint())); + endpoint->mutable_ipv4()->set_ipv4port(i); + } + endpoints->set_version(2); + + return endpoints; + } + + std::shared_ptr + buildTransaction(Logs &logs) { + Env env(*this, envconfig()); + int fund = 10000; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(fund), "alice", "bob"); + env.trust(bob["USD"](fund), alice); + env.close(); + + auto toBinary = [](std::string const &text) { + std::string binary; + for (size_t i = 0; i < text.size(); ++i) { + unsigned int c = charUnHex(text[i]); + c = c << 4; + ++i; + c = c | charUnHex(text[i]); + binary.push_back(c); + } + + return binary; + }; + + std::string usdTxBlob = ""; + auto wsc = makeWSClient(env.app().config()); + { + Json::Value jrequestUsd; + jrequestUsd[jss::secret] = toBase58(generateSeed("bob")); + jrequestUsd[jss::tx_json] = + pay("bob", "alice", bob["USD"](fund / 2)); + Json::Value jreply_usd = wsc->invoke("sign", jrequestUsd); + + usdTxBlob = + toBinary(jreply_usd[jss::result][jss::tx_blob].asString()); + } + + auto transaction = std::make_shared(); + transaction->set_rawtransaction(usdTxBlob); + transaction->set_status(protocol::tsNEW); + auto tk = make_TimeKeeper(logs.journal("TimeKeeper")); + transaction->set_receivetimestamp(tk->now().time_since_epoch().count()); + transaction->set_deferred(true); + + return transaction; + } + + std::shared_ptr + buildGetLedger() { + auto getLedger = std::make_shared(); + getLedger->set_itype(protocol::liTS_CANDIDATE); + getLedger->set_ltype(protocol::TMLedgerType::ltACCEPTED); + uint256 const hash(ripple::sha512Half(123456789)); + getLedger->set_ledgerhash(hash.begin(), hash.size()); + getLedger->set_ledgerseq(123456789); + ripple::SHAMapNodeID sha(hash.data(), hash.size()); + getLedger->add_nodeids(sha.getRawString()); + getLedger->set_requestcookie(123456789); + getLedger->set_querytype(protocol::qtINDIRECT); + getLedger->set_querydepth(3); + return getLedger; + } + + std::shared_ptr + buildLedgerData(uint32_t n, Logs &logs) { + auto ledgerData = std::make_shared(); + uint256 const hash(ripple::sha512Half(12356789)); + ledgerData->set_ledgerhash(hash.data(), hash.size()); + ledgerData->set_ledgerseq(123456789); + ledgerData->set_type(protocol::TMLedgerInfoType::liAS_NODE); + ledgerData->set_requestcookie(123456789); + ledgerData->set_error(protocol::TMReplyError::reNO_LEDGER); + ledgerData->mutable_nodes()->Reserve(n); + uint256 parentHash(0); + for (int i = 0; i < n; i++) { + LedgerInfo info; + auto tk = make_TimeKeeper(logs.journal("TimeKeeper")); + info.seq = i; + info.parentCloseTime = tk->now(); + info.hash = ripple::sha512Half(i); + info.txHash = ripple::sha512Half(i + 1); + info.accountHash = ripple::sha512Half(i + 2); + info.parentHash = parentHash; + info.drops = XRPAmount(10); + info.closeTimeResolution = tk->now().time_since_epoch(); + info.closeTime = tk->now(); + parentHash = ledgerHash(info); + Serializer nData; + ripple::addRaw(info, nData); + ledgerData->add_nodes()->set_nodedata(nData.getDataPtr(), nData.getLength()); + } + + return ledgerData; + } + + std::shared_ptr + buildGetObjectByHash() { + auto getObject = std::make_shared(); + + getObject->set_type(protocol::TMGetObjectByHash_ObjectType::TMGetObjectByHash_ObjectType_otTRANSACTION); + getObject->set_query(true); + getObject->set_seq(123456789); + uint256 hash(ripple::sha512Half(123456789)); + getObject->set_ledgerhash(hash.data(), hash.size()); + getObject->set_fat(true); + for (int i = 0; i < 100; i++) { + uint256 hash(ripple::sha512Half(i)); + auto object = getObject->add_objects(); + object->set_hash(hash.data(), hash.size()); + ripple::SHAMapNodeID sha(hash.data(), hash.size()); + object->set_nodeid(sha.getRawString()); + object->set_index(""); + object->set_data(""); + object->set_ledgerseq(i); + } + return getObject; + } + + std::shared_ptr + buildValidatorList() + { + auto list = std::make_shared(); + + auto master = randomKeyPair(KeyType::ed25519); + auto signing = randomKeyPair(KeyType::ed25519); + STObject st(sfGeneric); + st[sfSequence] = 0; + st[sfPublicKey] = std::get<0>(master); + st[sfSigningPubKey] = std::get<0>(signing); + st[sfDomain] = makeSlice(std::string("example.com")); + sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(master), sfMasterSignature); + sign(st, HashPrefix::manifest, KeyType::ed25519, std::get<1>(signing)); + Serializer s; + st.add(s); + list->set_manifest(s.data(), s.size()); + list->set_version(3); + STObject signature(sfSignature); + ripple::sign(st, HashPrefix::manifest,KeyType::ed25519, std::get<1>(signing)); + Serializer s1; + st.add(s1); + list->set_signature(s1.data(), s1.size()); + list->set_blob(strHex(s.getString())); + return list; + } + + void + testProtocol() { + testcase("Message Compression"); + + auto thresh = beast::severities::Severity::kInfo; + auto logs = std::make_unique(thresh); + + protocol::TMManifests manifests; + protocol::TMEndpoints endpoints; + protocol::TMTransaction transaction; + protocol::TMGetLedger get_ledger; + protocol::TMLedgerData ledger_data; + protocol::TMGetObjectByHash get_object; + protocol::TMValidatorList validator_list; + + // 4.5KB + doTest(buildManifests(20), protocol::mtMANIFESTS, 4, "TMManifests20"); + // 22KB + doTest(buildManifests(100), protocol::mtMANIFESTS, 4, "TMManifests100"); + // 131B + doTest(buildEndpoints(10), protocol::mtENDPOINTS, 4, "TMEndpoints10"); + // 1.3KB + doTest(buildEndpoints(100), protocol::mtENDPOINTS, 4, "TMEndpoints100"); + // 242B + doTest(buildTransaction(*logs), protocol::mtTRANSACTION, 1, "TMTransaction"); + // 87B + doTest(buildGetLedger(), protocol::mtGET_LEDGER, 1, "TMGetLedger"); + // 61KB + doTest(buildLedgerData(500, *logs), protocol::mtLEDGER_DATA, 10, "TMLedgerData500"); + // 122 KB + doTest(buildLedgerData(1000, *logs), protocol::mtLEDGER_DATA, 20, "TMLedgerData1000"); + // 1.2MB + doTest(buildLedgerData(10000, *logs), protocol::mtLEDGER_DATA, 50, "TMLedgerData10000"); + // 12MB + doTest(buildLedgerData(100000, *logs), protocol::mtLEDGER_DATA, 100, "TMLedgerData100000"); + // 61MB + doTest(buildLedgerData(500000, *logs), protocol::mtLEDGER_DATA, 100, "TMLedgerData500000"); + // 7.7KB + doTest(buildGetObjectByHash(), protocol::mtGET_OBJECTS, 4, "TMGetObjectByHash"); + // 895B + doTest(buildValidatorList(), protocol::mtVALIDATORLIST, 4, "TMValidatorList"); + } + + void run() override { + testProtocol(); + } + +}; + +BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(compression, ripple_data, ripple, 20); + +} +} \ No newline at end of file From 3e9cff92873003c5b98b1c137d6914394b0c6ed1 Mon Sep 17 00:00:00 2001 From: John Freeman Date: Mon, 16 Mar 2020 11:04:06 -0500 Subject: [PATCH 06/14] Fix Doxygen build --- .github/workflows/doxygen.yml | 23 ++++ Builds/CMake/RippledDocs.cmake | 101 ++++++++++++---- bin/ci/ubuntu/build-and-test.sh | 2 +- docs/Dockerfile | 5 +- docs/{source.dox => Doxyfile} | 112 +++++------------- docs/README.md | 35 +++--- src/ripple/app/ledger/impl/InboundLedgers.cpp | 1 + 7 files changed, 150 insertions(+), 129 deletions(-) create mode 100644 .github/workflows/doxygen.yml rename docs/{source.dox => Doxyfile} (77%) diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml new file mode 100644 index 00000000000..7a21548bf0d --- /dev/null +++ b/.github/workflows/doxygen.yml @@ -0,0 +1,23 @@ +name: Build and publish Doxygen documentation +on: + push: + branches: + - develop + +jobs: + job: + runs-on: ubuntu-18.04 + steps: + - name: checkout + uses: actions/checkout@v2 + - name: set OUTPUT_DIRECTORY + run: echo 'OUTPUT_DIRECTORY = html' >> docs/Doxyfile + - name: build + uses: mattnotmitt/doxygen-action@v1 + with: + doxyfile-path: 'docs/Doxyfile' + - name: publish + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./html diff --git a/Builds/CMake/RippledDocs.cmake b/Builds/CMake/RippledDocs.cmake index be964a56049..b168b8d0154 100644 --- a/Builds/CMake/RippledDocs.cmake +++ b/Builds/CMake/RippledDocs.cmake @@ -3,29 +3,82 @@ #]===================================================================] find_package (Doxygen) -if (TARGET Doxygen::doxygen) - set (doc_srcs docs/source.dox) - file (GLOB_RECURSE other_docs docs/*.md) - list (APPEND doc_srcs "${other_docs}") - # read the source config and make a modified one - # that points the output files to our build directory - file (READ "${CMAKE_CURRENT_SOURCE_DIR}/docs/source.dox" dox_content) - string (REGEX REPLACE "[\t ]*OUTPUT_DIRECTORY[\t ]*=(.*)" - "OUTPUT_DIRECTORY=${CMAKE_BINARY_DIR}\n\\1" - new_config "${dox_content}") - file (WRITE "${CMAKE_BINARY_DIR}/source.dox" "${new_config}") - add_custom_target (docs - COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_BINARY_DIR}/source.dox" - BYPRODUCTS "${CMAKE_BINARY_DIR}/html_doc/index.html" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs" - SOURCES "${doc_srcs}") - if (is_multiconfig) - set_property ( - SOURCE ${doc_srcs} - APPEND - PROPERTY HEADER_FILE_ONLY - true) - endif () -else () +if (NOT TARGET Doxygen::doxygen) message (STATUS "doxygen executable not found -- skipping docs target") + return () +endif () + +set (doxygen_output_directory "${CMAKE_BINARY_DIR}/docs") +set (doxygen_include_path "${CMAKE_SOURCE_DIR}/src") +set (doxygen_index_file "${doxygen_output_directory}/html/index.html") +set (doxyfile "${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile") + +file (GLOB_RECURSE doxygen_input + docs/*.md + src/ripple/*.h + src/ripple/*.cpp + src/ripple/*.md + src/test/*.h + src/test/*.md + Builds/*/README.md) +list (APPEND doxygen_input + README.md + RELEASENOTES.md + src/README.md) +set (dependencies "${doxygen_input}" "${doxyfile}") + +function (verbose_find_path variable name) + # find_path sets a CACHE variable, so don't try using a "local" variable. + find_path (${variable} "${name}" ${ARGN}) + if (NOT ${variable}) + message (WARNING "could not find ${name}") + else () + message (STATUS "found ${name}: ${${variable}}/${name}") + endif () +endfunction () + +verbose_find_path (doxygen_plantuml_jar_path plantuml.jar PATH_SUFFIXES share/plantuml) +verbose_find_path (doxygen_dot_path dot) + +# https://en.cppreference.com/w/Cppreference:Archives +# https://stackoverflow.com/questions/60822559/how-to-move-a-file-download-from-configure-step-to-build-step +set (download_script "${CMAKE_BINARY_DIR}/docs/download-cppreference.cmake") +file (WRITE + "${download_script}" + "file (DOWNLOAD \ + http://upload.cppreference.com/mwiki/images/b/b2/html_book_20190607.zip \ + ${CMAKE_BINARY_DIR}/docs/cppreference.zip \ + EXPECTED_HASH MD5=82b3a612d7d35a83e3cb1195a63689ab \ + )\n \ + execute_process ( \ + COMMAND \"${CMAKE_COMMAND}\" -E tar -xf cppreference.zip \ + )\n" +) +set (tagfile "${CMAKE_BINARY_DIR}/docs/cppreference-doxygen-web.tag.xml") +add_custom_command ( + OUTPUT "${tagfile}" + COMMAND "${CMAKE_COMMAND}" -P "${download_script}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/docs" +) +set (doxygen_tagfiles "${tagfile}=http://en.cppreference.com/w/") + +add_custom_command ( + OUTPUT "${doxygen_index_file}" + COMMAND "${CMAKE_COMMAND}" -E env + "DOXYGEN_OUTPUT_DIRECTORY=${doxygen_output_directory}" + "DOXYGEN_INCLUDE_PATH=${doxygen_include_path}" + "DOXYGEN_TAGFILES=${doxygen_tagfiles}" + "DOXYGEN_PLANTUML_JAR_PATH=${doxygen_plantuml_jar_path}" + "DOXYGEN_DOT_PATH=${doxygen_dot_path}" + "${DOXYGEN_EXECUTABLE}" "${doxyfile}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + DEPENDS "${dependencies}" "${tagfile}") +add_custom_target (docs + DEPENDS "${doxygen_index_file}" + SOURCES "${dependencies}") +if (is_multiconfig) + set_property ( + SOURCE ${dependencies} + APPEND PROPERTY + HEADER_FILE_ONLY true) endif () diff --git a/bin/ci/ubuntu/build-and-test.sh b/bin/ci/ubuntu/build-and-test.sh index 41f433ef1ae..7ffad801dd1 100755 --- a/bin/ci/ubuntu/build-and-test.sh +++ b/bin/ci/ubuntu/build-and-test.sh @@ -105,7 +105,7 @@ ${time} eval cmake --build . ${BUILDARGS} -- ${BUILDTOOLARGS} if [[ ${TARGET} == "docs" ]]; then ## mimic the standard test output for docs build ## to make controlling processes like jenkins happy - if [ -f html_doc/index.html ]; then + if [ -f docs/html/index.html ]; then echo "1 case, 1 test total, 0 failures" else echo "1 case, 1 test total, 1 failures" diff --git a/docs/Dockerfile b/docs/Dockerfile index dd33d95e647..d716ca21315 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -26,6 +26,7 @@ RUN rm -rf /tmp/doxygen-1.8.14 RUN mkdir -p /opt/plantuml RUN wget -O /opt/plantuml/plantuml.jar http://sourceforge.net/projects/plantuml/files/plantuml.jar/download -ENV PLANTUML_JAR=/opt/plantuml/plantuml.jar +ENV DOXYGEN_PLANTUML_JAR_PATH=/opt/plantuml/plantuml.jar -CMD cd /opt/rippled/docs && doxygen source.dox +ENV DOXYGEN_OUTPUT_DIRECTORY=html +CMD cd /opt/rippled && doxygen docs/Doxyfile diff --git a/docs/source.dox b/docs/Doxyfile similarity index 77% rename from docs/source.dox rename to docs/Doxyfile index fb2fa12ff6e..48a0b5d1e1a 100644 --- a/docs/source.dox +++ b/docs/Doxyfile @@ -4,10 +4,10 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "rippled" PROJECT_NUMBER = -PROJECT_BRIEF = C++ Library +PROJECT_BRIEF = PROJECT_LOGO = -PROJECT_LOGO = images/LogoForDocumentation.png -OUTPUT_DIRECTORY = +PROJECT_LOGO = +OUTPUT_DIRECTORY = $(DOXYGEN_OUTPUT_DIRECTORY) CREATE_SUBDIRS = NO ALLOW_UNICODE_NAMES = NO OUTPUT_LANGUAGE = English @@ -17,7 +17,7 @@ ABBREVIATE_BRIEF = ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = YES FULL_PATH_NAMES = NO -STRIP_FROM_PATH = ../src/ +STRIP_FROM_PATH = src/ STRIP_FROM_INC_PATH = SHORT_NAMES = NO JAVADOC_AUTOBRIEF = YES @@ -27,7 +27,6 @@ INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO TAB_SIZE = 4 ALIASES = -TCL_SUBST = OPTIMIZE_OUTPUT_FOR_C = NO OPTIMIZE_OUTPUT_JAVA = NO OPTIMIZE_FOR_FORTRAN = NO @@ -35,7 +34,7 @@ OPTIMIZE_OUTPUT_VHDL = NO EXTENSION_MAPPING = MARKDOWN_SUPPORT = YES AUTOLINK_SUPPORT = YES -BUILTIN_STL_SUPPORT = NO +BUILTIN_STL_SUPPORT = YES CPP_CLI_SUPPORT = NO SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES @@ -83,7 +82,7 @@ ENABLED_SECTIONS = MAX_INITIALIZER_LINES = 30 SHOW_USED_FILES = NO SHOW_FILES = NO -SHOW_NAMESPACES = NO +SHOW_NAMESPACES = YES FILE_VERSION_FILTER = LAYOUT_FILE = CITE_BIB_FILES = @@ -104,71 +103,17 @@ WARN_LOGFILE = # Configuration options related to the input files #--------------------------------------------------------------------------- INPUT = \ -\ - ../src/ripple/app/misc/TxQ.h \ - ../src/ripple/app/tx/apply.h \ - ../src/ripple/app/tx/applySteps.h \ - ../src/ripple/protocol/STObject.h \ - ../src/ripple/protocol/JsonFields.h \ - ../src/test/jtx/AbstractClient.h \ - ../src/test/jtx/JSONRPCClient.h \ - ../src/test/jtx/WSClient.h \ - ../src/ripple/consensus/Consensus.h \ - ../src/ripple/consensus/ConsensusProposal.h \ - ../src/ripple/consensus/ConsensusTypes.h \ - ../src/ripple/consensus/DisputedTx.h \ - ../src/ripple/consensus/LedgerTiming.h \ - ../src/ripple/consensus/LedgerTrie.h \ - ../src/ripple/consensus/Validations.h \ - ../src/ripple/consensus/ConsensusParms.h \ - ../src/ripple/app/consensus/RCLCxTx.h \ - ../src/ripple/app/consensus/RCLCxLedger.h \ - ../src/ripple/app/consensus/RCLConsensus.h \ - ../src/ripple/app/consensus/RCLCxPeerPos.h \ - ../src/ripple/app/tx/apply.h \ - ../src/ripple/app/tx/applySteps.h \ - ../src/ripple/app/tx/impl/InvariantCheck.h \ - ../src/ripple/app/consensus/RCLValidations.h \ - ../src/README.md \ - ../src/ripple/README.md \ - ../README.md \ - ../RELEASENOTES.md \ - ../docs/CodingStyle.md \ - ../docs/CheatSheet.md \ - ../docs/README.md \ - ../docs/sample_chart.doc \ - ../docs/HeapProfiling.md \ - ../docs/Docker.md \ - ../docs/consensus.md \ - ../Builds/macos/README.md \ - ../Builds/linux/README.md \ - ../Builds/VisualStudio2017/README.md \ - ../src/ripple/consensus/README.md \ - ../src/ripple/app/consensus/README.md \ - ../src/test/csf/README.md \ - ../src/ripple/basics/README.md \ - ../src/ripple/crypto/README.md \ - ../src/ripple/peerfinder/README.md \ - ../src/ripple/app/misc/README.md \ - ../src/ripple/app/misc/FeeEscalation.md \ - ../src/ripple/app/ledger/README.md \ - ../src/ripple/app/paths/README.md \ - ../src/ripple/app/tx/README.md \ - ../src/ripple/proto/README.md \ - ../src/ripple/shamap/README.md \ - ../src/ripple/protocol/README.md \ - ../src/ripple/json/README.md \ - ../src/ripple/json/TODO.md \ - ../src/ripple/resource/README.md \ - ../src/ripple/rpc/README.md \ - ../src/ripple/overlay/README.md \ - ../src/ripple/nodestore/README.md \ - ../src/ripple/nodestore/Benchmarks.md \ + docs \ + src/ripple \ + src/test \ + src/README.md \ + README.md \ + RELEASENOTES.md \ INPUT_ENCODING = UTF-8 -FILE_PATTERNS = -RECURSIVE = NO +FILE_PATTERNS = *.h *.cpp *.md +RECURSIVE = YES EXCLUDE = EXCLUDE_SYMLINKS = NO EXCLUDE_PATTERNS = @@ -177,20 +122,20 @@ EXAMPLE_PATH = EXAMPLE_PATTERNS = EXAMPLE_RECURSIVE = NO IMAGE_PATH = \ - ./images/ \ - ./images/consensus/ \ - ../src/test/csf/ \ + docs/images/ \ + docs/images/consensus/ \ + src/test/csf/ \ INPUT_FILTER = FILTER_PATTERNS = FILTER_SOURCE_FILES = NO FILTER_SOURCE_PATTERNS = -USE_MDFILE_AS_MAINPAGE = ../src/README.md +USE_MDFILE_AS_MAINPAGE = src/README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- -SOURCE_BROWSER = NO +SOURCE_BROWSER = YES INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES REFERENCED_BY_RELATION = NO @@ -213,7 +158,7 @@ IGNORE_PREFIX = # Configuration options related to the HTML output #--------------------------------------------------------------------------- GENERATE_HTML = YES -HTML_OUTPUT = html_doc +HTML_OUTPUT = html HTML_FILE_EXTENSION = .html HTML_HEADER = HTML_FOOTER = @@ -273,7 +218,7 @@ EXTRA_SEARCH_MAPPINGS = #--------------------------------------------------------------------------- GENERATE_LATEX = NO LATEX_OUTPUT = latex -LATEX_CMD_NAME = latex +LATEX_CMD_NAME = MAKEINDEX_CMD_NAME = makeindex COMPACT_LATEX = NO PAPER_TYPE = a4 @@ -314,7 +259,7 @@ MAN_LINKS = NO # Configuration options related to the XML output #--------------------------------------------------------------------------- GENERATE_XML = NO -XML_OUTPUT = temp +XML_OUTPUT = xml XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- @@ -340,7 +285,7 @@ ENABLE_PREPROCESSING = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = YES -INCLUDE_PATH = ../ +INCLUDE_PATH = $(DOXYGEN_INCLUDE_PATH) INCLUDE_FILE_PATTERNS = PREDEFINED = DOXYGEN \ GENERATING_DOCS \ @@ -353,21 +298,20 @@ SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration options related to external references #--------------------------------------------------------------------------- -TAGFILES = +TAGFILES = $(DOXYGEN_TAGFILES) GENERATE_TAGFILE = ALLEXTERNALS = NO EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES -PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- CLASS_DIAGRAMS = NO -MSCGEN_PATH = DIA_PATH = HIDE_UNDOC_RELATIONS = YES -HAVE_DOT = NO +HAVE_DOT = YES +# DOT_NUM_THREADS = 0 means 1 for every processor. DOT_NUM_THREADS = 0 DOT_FONTNAME = Helvetica DOT_FONTSIZE = 10 @@ -386,11 +330,11 @@ GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES DOT_IMAGE_FORMAT = png INTERACTIVE_SVG = NO -DOT_PATH = +DOT_PATH = $(DOXYGEN_DOT_PATH) DOTFILE_DIRS = MSCFILE_DIRS = DIAFILE_DIRS = -PLANTUML_JAR_PATH = $(PLANTUML_JAR) +PLANTUML_JAR_PATH = $(DOXYGEN_PLANTUML_JAR_PATH) PLANTUML_INCLUDE_PATH = DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 diff --git a/docs/README.md b/docs/README.md index 04e25775273..3345ee828f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,5 @@ # Building documentation -## Specifying Files - -To specify the source files for which to build documentation, modify `INPUT` -and its related fields in `docs/source.dox`. Note that the `INPUT` paths are -relative to the `docs/` directory. - ## Install Dependencies ### Windows @@ -30,32 +24,38 @@ Install these dependencies: ### [Optional] Install Plantuml (all platforms) -Doxygen supports the optional use of [plantuml](http://plantuml.com) to +Doxygen supports the optional use of [plantuml](http://plantuml.com) to generate diagrams from `@startuml` sections. We don't currently rely on this functionality for docs, so it's largely optional. Requirements: 1. Download/install a functioning java runtime, if you don't already have one. 2. Download [plantuml](http://plantuml.com) from [here](http://sourceforge.net/projects/plantuml/files/plantuml.jar/download). - Set a system environment variable named `PLANTUML_JAR` with a value of the fullpath - to the file system location of the `plantuml.jar` file you downloaded. + Set a system environment variable named `DOXYGEN_PLANTUML_JAR_PATH` to + the absolute path of the `plantuml.jar` file you downloaded. + + +## Configure + +You should set these environment variables: -## Do it +- `DOXYGEN_OUTPUT_DIRECTORY` +- `DOXYGEN_PLANTUML_JAR_PATH` -### all platforms +## Build From the rippled root folder: + ``` -cd docs -mkdir -p html_doc -doxygen source.dox +doxygen docs/Doxyfile ``` -The output will be in `docs/html_doc`. + +The output will be wherever you chose for `DOXYGEN_OUTPUT_DIRECTORY`. ## Docker (applicable to all platforms) - + Instead of installing the doxygen tools locally, you can use the provided `Dockerfile` to create an ubuntu based image for running the tools: @@ -72,5 +72,4 @@ Then to run the image, from the rippled root folder: sudo docker run -v $PWD:/opt/rippled --rm rippled-docs ``` -The output will be in `docs/html_doc`. - +The output will be in `html`. diff --git a/src/ripple/app/ledger/impl/InboundLedgers.cpp b/src/ripple/app/ledger/impl/InboundLedgers.cpp index c126bc9c325..cba1301afb9 100644 --- a/src/ripple/app/ledger/impl/InboundLedgers.cpp +++ b/src/ripple/app/ledger/impl/InboundLedgers.cpp @@ -64,6 +64,7 @@ class InboundLedgersImp { } + /** @callgraph */ std::shared_ptr acquire(uint256 const& hash, std::uint32_t seq, InboundLedger::Reason reason) override From 25b13978e78fe58219d6e7647ba3661b9289c4de Mon Sep 17 00:00:00 2001 From: John Freeman Date: Tue, 7 Apr 2020 09:07:49 -0500 Subject: [PATCH 07/14] Fix unity build --- Builds/CMake/RippledCore.cmake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index db56e1cf287..56502aa6690 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -965,7 +965,8 @@ endif () if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16) # any files that don't play well with unity should be added here set_source_files_properties( - # this one seems to produce conflicts in beast teardown template methods: + # these two seem to produce conflicts in beast teardown template methods src/test/rpc/ValidatorRPC_test.cpp + src/test/rpc/ShardArchiveHandler_test.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION TRUE) endif () From 9e5f1c5b625c85e9d716cb0f13854d52d291fb7a Mon Sep 17 00:00:00 2001 From: rabbit <18340247+crypticrabbit@users.noreply.github.com> Date: Thu, 2 Apr 2020 12:51:13 -0400 Subject: [PATCH 08/14] Adding devnet as an option to [network_id]: *The [network_id] option allows three string values: "main", "testnet", and "devnet" in addition to unsigned integers. --- cfg/rippled-example.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/cfg/rippled-example.cfg b/cfg/rippled-example.cfg index 43982b9a951..3c856b40fd8 100644 --- a/cfg/rippled-example.cfg +++ b/cfg/rippled-example.cfg @@ -746,6 +746,7 @@ # # main -> 0 # testnet -> 1 +# devnet -> 2 # # If this value is not specified the server is not explicitly configured # to track a particular network. From f88e5d7f201a3c6641d34600f46536d2fce1f600 Mon Sep 17 00:00:00 2001 From: Mark Travis Date: Mon, 6 Apr 2020 15:03:46 -0700 Subject: [PATCH 09/14] Maintain history back to the earliest persisted ledger: This makes behavior consistent with configurations both with and without online delete. --- src/ripple/app/ledger/LedgerMaster.h | 7 +- src/ripple/app/ledger/impl/LedgerMaster.cpp | 82 ++++++++++++++------- src/ripple/app/misc/SHAMapStore.h | 4 +- src/ripple/app/misc/SHAMapStoreImp.cpp | 28 +++---- src/ripple/app/misc/SHAMapStoreImp.h | 10 ++- 5 files changed, 83 insertions(+), 48 deletions(-) diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index fa9d428a154..0eb303207f3 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -39,6 +39,7 @@ #include #include #include +#include #include @@ -280,11 +281,6 @@ class LedgerMaster // Try to publish ledgers, acquire missing ledgers. Always called with // m_mutex locked. The passed lock is a reminder to callers. void doAdvance(std::unique_lock&); - bool shouldAcquire( - std::uint32_t const currentLedger, - std::uint32_t const ledgerHistory, - std::uint32_t const ledgerHistoryIndex, - std::uint32_t const candidateLedger) const; std::vector> findNewLedgersToPublish(std::unique_lock&); @@ -295,7 +291,6 @@ class LedgerMaster // The passed lock is a reminder to callers. bool newPFWork(const char *name, std::unique_lock&); -private: Application& app_; beast::Journal m_journal; diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index f0f63e877fd..373d61896cf 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,7 @@ #include #include #include +#include #include #include @@ -140,6 +142,57 @@ static constexpr std::chrono::minutes MAX_LEDGER_AGE_ACQUIRE {1}; // Don't acquire history if write load is too high static constexpr int MAX_WRITE_LOAD_ACQUIRE {8192}; +// Helper function for LedgerMaster::doAdvance() +// Returns the minimum ledger sequence in SQL database, if any. +static boost::optional +minSqlSeq(Application& app) +{ + boost::optional seq; + auto db = app.getLedgerDB().checkoutDb(); + *db << "SELECT MIN(LedgerSeq) FROM Ledgers", soci::into(seq); + return seq; +} + +// Helper function for LedgerMaster::doAdvance() +// Return true if candidateLedger should be fetched from the network. +static bool +shouldAcquire ( + std::uint32_t const currentLedger, + std::uint32_t const ledgerHistory, + boost::optional minSeq, + std::uint32_t const lastRotated, + std::uint32_t const candidateLedger, + beast::Journal j) +{ + bool ret = [&]() + { + // Fetch ledger if it may be the current ledger + if (candidateLedger >= currentLedger) + return true; + + // Or if it is within our configured history range: + if (currentLedger - candidateLedger <= ledgerHistory) + return true; + + // Or it's greater than or equal to both: + // - the minimum persisted ledger or the maximum possible + // sequence value, if no persisted ledger, and + // - minimum ledger that will be persisted as of the next online + // deletion interval, or 1 if online deletion is disabled. + return + candidateLedger >= std::max( + minSeq.value_or(std::numeric_limits::max()), + lastRotated + 1); + }(); + + JLOG (j.trace()) + << "Missing ledger " + << candidateLedger + << (ret ? " should" : " should NOT") + << " be acquired"; + return ret; +} + LedgerMaster::LedgerMaster (Application& app, Stopwatch& stopwatch, Stoppable& parent, beast::insight::Collector::ptr const& collector, beast::Journal journal) @@ -1697,31 +1750,6 @@ LedgerMaster::releaseReplay () return std::move (replayData); } -bool -LedgerMaster::shouldAcquire ( - std::uint32_t const currentLedger, - std::uint32_t const ledgerHistory, - std::uint32_t const ledgerHistoryIndex, - std::uint32_t const candidateLedger) const -{ - - // Fetch ledger if it might be the current ledger, - // is requested by the advisory delete setting, or - // is within our configured history range - - bool ret (candidateLedger >= currentLedger || - ((ledgerHistoryIndex > 0) && - (candidateLedger > ledgerHistoryIndex)) || - (currentLedger - candidateLedger) <= ledgerHistory); - - JLOG (m_journal.trace()) - << "Missing ledger " - << candidateLedger - << (ret ? " should" : " should NOT") - << " be acquired"; - return ret; -} - void LedgerMaster::fetchForHistory( std::uint32_t missing, @@ -1875,7 +1903,9 @@ void LedgerMaster::doAdvance (std::unique_lock& sl) "tryAdvance discovered missing " << *missing; if ((mFillInProgress == 0 || *missing > mFillInProgress) && shouldAcquire(mValidLedgerSeq, ledger_history_, - app_.getSHAMapStore().getCanDelete(), *missing)) + minSqlSeq(app_), + app_.getSHAMapStore().getLastRotated(), *missing, + m_journal)) { JLOG(m_journal.trace()) << "advanceThread should acquire"; diff --git a/src/ripple/app/misc/SHAMapStore.h b/src/ripple/app/misc/SHAMapStore.h index b41e9c4ca17..0814b98df57 100644 --- a/src/ripple/app/misc/SHAMapStore.h +++ b/src/ripple/app/misc/SHAMapStore.h @@ -56,7 +56,9 @@ class SHAMapStore /** Whether advisory delete is enabled. */ virtual bool advisoryDelete() const = 0; - /** Last ledger which was copied during rotation of backends. */ + /** Maximum ledger that has been deleted, or will be deleted if + * currently in the act of online deletion. + */ virtual LedgerIndex getLastRotated() = 0; /** Highest ledger that may be deleted. */ diff --git a/src/ripple/app/misc/SHAMapStoreImp.cpp b/src/ripple/app/misc/SHAMapStoreImp.cpp index 0027bd021c7..5c2644e6353 100644 --- a/src/ripple/app/misc/SHAMapStoreImp.cpp +++ b/src/ripple/app/misc/SHAMapStoreImp.cpp @@ -322,7 +322,7 @@ void SHAMapStoreImp::run() { beast::setCurrentThreadName ("SHAMapStore"); - LedgerIndex lastRotated = state_db_.getState().lastRotated; + lastRotated_ = state_db_.getState().lastRotated; netOPs_ = &app_.getOPs(); ledgerMaster_ = &app_.getLedgerMaster(); fullBelowCache_ = &app_.family().fullbelow(); @@ -333,7 +333,7 @@ SHAMapStoreImp::run() if (advisoryDelete_) canDelete_ = state_db_.getCanDelete (); - while (1) + while (true) { healthy_ = true; std::shared_ptr validatedLedger; @@ -356,20 +356,20 @@ SHAMapStoreImp::run() continue; } - LedgerIndex validatedSeq = validatedLedger->info().seq; - if (!lastRotated) + LedgerIndex const validatedSeq = validatedLedger->info().seq; + if (!lastRotated_) { - lastRotated = validatedSeq; - state_db_.setLastRotated (lastRotated); + lastRotated_ = validatedSeq; + state_db_.setLastRotated (lastRotated_); } - // will delete up to (not including) lastRotated) - if (validatedSeq >= lastRotated + deleteInterval_ - && canDelete_ >= lastRotated - 1) + // will delete up to (not including) lastRotated_ + if (validatedSeq >= lastRotated_ + deleteInterval_ + && canDelete_ >= lastRotated_ - 1) { JLOG(journal_.warn()) << "rotating validatedSeq " << validatedSeq - << " lastRotated " << lastRotated << " deleteInterval " - << deleteInterval_ << " canDelete_ " << canDelete_; + << " lastRotated_ " << lastRotated_ << " deleteInterval " + << deleteInterval_ << " canDelete_ " << canDelete_; switch (health()) { @@ -383,7 +383,7 @@ SHAMapStoreImp::run() ; } - clearPrior (lastRotated); + clearPrior (lastRotated_); switch (health()) { case Health::stopping: @@ -448,13 +448,13 @@ SHAMapStoreImp::run() std::string nextArchiveDir = dbRotating_->getWritableBackend()->getName(); - lastRotated = validatedSeq; + lastRotated_ = validatedSeq; std::shared_ptr oldBackend; { std::lock_guard lock (dbRotating_->peekMutex()); state_db_.setState (SavedState {newBackend->getName(), - nextArchiveDir, lastRotated}); + nextArchiveDir, lastRotated_}); clearCaches (validatedSeq); oldBackend = dbRotating_->rotateBackends( std::move(newBackend), diff --git a/src/ripple/app/misc/SHAMapStoreImp.h b/src/ripple/app/misc/SHAMapStoreImp.h index ea2579dd005..5c8454b356c 100644 --- a/src/ripple/app/misc/SHAMapStoreImp.h +++ b/src/ripple/app/misc/SHAMapStoreImp.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -84,6 +85,13 @@ class SHAMapStoreImp : public SHAMapStore static std::uint32_t const minimumDeletionInterval_ = 256; // minimum # of ledgers required for standalone mode. static std::uint32_t const minimumDeletionIntervalSA_ = 8; + // Ledger sequence at which the last deletion interval was triggered, + // or the current validated sequence as of first use + // if there have been no prior deletions. Deletion occurs up to (but + // not including) this value. All ledgers past this value are accumulated + // until the next online deletion. This value is persisted to SQLite + // nearly immediately after modification. + std::atomic lastRotated_ {}; NodeStore::Scheduler& scheduler_; beast::Journal const journal_; @@ -159,7 +167,7 @@ class SHAMapStoreImp : public SHAMapStore LedgerIndex getLastRotated() override { - return state_db_.getState().lastRotated; + return lastRotated_; } // All ledgers before and including this are unprotected From 03711a9d4a61224a4f8d7942baaad7cbf2110d8e Mon Sep 17 00:00:00 2001 From: Howard Hinnant Date: Thu, 5 Mar 2020 15:53:12 -0500 Subject: [PATCH 10/14] Rename canonicalize into two functions: * canonicalize_replace_cache * canonicalize_replace_client Now it is clear at the call site that if there are duplicate copies of the data between the cache and the caller, which copy gets replaced. Additionally data parameter is now const-correct. If it is not going to be replaced (canonicalize_replace_cache), then the shared_ptr to the client data is const. --- src/ripple/app/ledger/Ledger.cpp | 2 +- src/ripple/app/ledger/LedgerHistory.cpp | 12 ++++----- src/ripple/app/ledger/impl/LedgerMaster.cpp | 2 +- .../app/ledger/impl/TransactionMaster.cpp | 6 ++--- src/ripple/app/misc/NetworkOPs.cpp | 2 +- src/ripple/basics/TaggedCache.h | 26 ++++++++++++++++--- src/ripple/nodestore/impl/Database.cpp | 4 +-- src/ripple/nodestore/impl/DatabaseNodeImp.cpp | 2 +- .../nodestore/impl/DatabaseRotatingImp.cpp | 2 +- .../nodestore/impl/DatabaseShardImp.cpp | 2 +- src/ripple/shamap/impl/SHAMap.cpp | 2 +- src/test/basics/TaggedCache_test.cpp | 4 +-- 12 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/ripple/app/ledger/Ledger.cpp b/src/ripple/app/ledger/Ledger.cpp index 6125149796f..f72d6d73472 100644 --- a/src/ripple/app/ledger/Ledger.cpp +++ b/src/ripple/app/ledger/Ledger.cpp @@ -835,7 +835,7 @@ static bool saveValidatedLedger ( if (! aLedger) { aLedger = std::make_shared(ledger, app.accountIDCache(), app.logs()); - app.getAcceptedLedgerCache().canonicalize(ledger->info().hash, aLedger); + app.getAcceptedLedgerCache().canonicalize_replace_client(ledger->info().hash, aLedger); } } catch (std::exception const&) diff --git a/src/ripple/app/ledger/LedgerHistory.cpp b/src/ripple/app/ledger/LedgerHistory.cpp index 95c35166b14..aa07e8ad40b 100644 --- a/src/ripple/app/ledger/LedgerHistory.cpp +++ b/src/ripple/app/ledger/LedgerHistory.cpp @@ -62,8 +62,8 @@ LedgerHistory::insert( std::unique_lock sl (m_ledgers_by_hash.peekMutex ()); - const bool alreadyHad = m_ledgers_by_hash.canonicalize ( - ledger->info().hash, ledger, true); + const bool alreadyHad = m_ledgers_by_hash.canonicalize_replace_cache( + ledger->info().hash, ledger); if (validated) mLedgersByIndex[ledger->info().seq] = ledger->info().hash; @@ -108,7 +108,7 @@ LedgerHistory::getLedgerBySeq (LedgerIndex index) std::unique_lock sl (m_ledgers_by_hash.peekMutex ()); assert (ret->isImmutable ()); - m_ledgers_by_hash.canonicalize (ret->info().hash, ret); + m_ledgers_by_hash.canonicalize_replace_client(ret->info().hash, ret); mLedgersByIndex[ret->info().seq] = ret->info().hash; return (ret->info().seq == index) ? ret : nullptr; } @@ -133,7 +133,7 @@ LedgerHistory::getLedgerByHash (LedgerHash const& hash) assert (ret->isImmutable ()); assert (ret->info().hash == hash); - m_ledgers_by_hash.canonicalize (ret->info().hash, ret); + m_ledgers_by_hash.canonicalize_replace_client(ret->info().hash, ret); assert (ret->info().hash == hash); return ret; @@ -432,7 +432,7 @@ void LedgerHistory::builtLedger ( m_consensus_validated.peekMutex()); auto entry = std::make_shared(); - m_consensus_validated.canonicalize(index, entry, false); + m_consensus_validated.canonicalize_replace_client(index, entry); if (entry->validated && ! entry->built) { @@ -472,7 +472,7 @@ void LedgerHistory::validatedLedger ( m_consensus_validated.peekMutex()); auto entry = std::make_shared(); - m_consensus_validated.canonicalize(index, entry, false); + m_consensus_validated.canonicalize_replace_client(index, entry); if (entry->built && ! entry->validated) { diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 373d61896cf..3b64f8b38d4 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -1976,7 +1976,7 @@ LedgerMaster::addFetchPack ( uint256 const& hash, std::shared_ptr< Blob >& data) { - fetch_packs_.canonicalize (hash, data); + fetch_packs_.canonicalize_replace_client(hash, data); } boost::optional diff --git a/src/ripple/app/ledger/impl/TransactionMaster.cpp b/src/ripple/app/ledger/impl/TransactionMaster.cpp index d6f7ab425b4..0bfc2500707 100644 --- a/src/ripple/app/ledger/impl/TransactionMaster.cpp +++ b/src/ripple/app/ledger/impl/TransactionMaster.cpp @@ -62,7 +62,7 @@ TransactionMaster::fetch (uint256 const& txnID, error_code_i& ec) if (!txn) return txn; - mCache.canonicalize (txnID, txn); + mCache.canonicalize_replace_client(txnID, txn); return txn; } @@ -82,7 +82,7 @@ TransactionMaster::fetch (uint256 const& txnID, ClosedInterval const& txnID, mApp, range, ec); if (v.which () == 0 && boost::get (v)) - mCache.canonicalize (txnID, boost::get (v)); + mCache.canonicalize_replace_client(txnID, boost::get (v)); return v; } @@ -127,7 +127,7 @@ TransactionMaster::canonicalize(std::shared_ptr* pTransaction) { auto txn = *pTransaction; // VFALCO NOTE canonicalize can change the value of txn! - mCache.canonicalize(tid, txn); + mCache.canonicalize_replace_client(tid, txn); *pTransaction = txn; } } diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 4a067d5fde9..b0ca0130900 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -2665,7 +2665,7 @@ void NetworkOPsImp::pubLedger ( { alpAccepted = std::make_shared ( lpAccepted, app_.accountIDCache(), app_.logs()); - app_.getAcceptedLedgerCache().canonicalize ( + app_.getAcceptedLedgerCache().canonicalize_replace_client( lpAccepted->info().hash, alpAccepted); } diff --git a/src/ripple/basics/TaggedCache.h b/src/ripple/basics/TaggedCache.h index 2d1723c839e..4898e9831d3 100644 --- a/src/ripple/basics/TaggedCache.h +++ b/src/ripple/basics/TaggedCache.h @@ -288,7 +288,14 @@ class TaggedCache @return `true` If the key already existed. */ - bool canonicalize (const key_type& key, std::shared_ptr& data, bool replace = false) +private: + + template + bool + canonicalize( + const key_type& key, + std::conditional_t const, std::shared_ptr>& data + ) { // Return canonical value, store if needed, refresh in cache // Return values: true=we had the data already @@ -310,7 +317,7 @@ class TaggedCache if (entry.isCached ()) { - if (replace) + if constexpr (replace) { entry.ptr = data; entry.weak_ptr = data; @@ -327,7 +334,7 @@ class TaggedCache if (cachedData) { - if (replace) + if constexpr (replace) { entry.ptr = data; entry.weak_ptr = data; @@ -349,6 +356,17 @@ class TaggedCache return false; } +public: + bool canonicalize_replace_cache(const key_type& key, std::shared_ptr const& data) + { + return canonicalize(key, data); + } + + bool canonicalize_replace_client(const key_type& key, std::shared_ptr& data) + { + return canonicalize(key, data); + } + std::shared_ptr fetch (const key_type& key) { // fetch us a shared pointer to the stored data object @@ -393,7 +411,7 @@ class TaggedCache { mapped_ptr p (std::make_shared ( std::cref (value))); - return canonicalize (key, p); + return canonicalize_replace_client(key, p); } // VFALCO NOTE It looks like this returns a copy of the data in diff --git a/src/ripple/nodestore/impl/Database.cpp b/src/ripple/nodestore/impl/Database.cpp index 1e8e01195c3..37f58a21204 100644 --- a/src/ripple/nodestore/impl/Database.cpp +++ b/src/ripple/nodestore/impl/Database.cpp @@ -216,7 +216,7 @@ Database::doFetch(uint256 const& hash, std::uint32_t seq, else { // Ensure all threads get the same object - pCache.canonicalize(hash, nObj); + pCache.canonicalize_replace_client(hash, nObj); // Since this was a 'hard' fetch, we will log it. JLOG(j_.trace()) << @@ -265,7 +265,7 @@ Database::storeLedger( { for (auto& nObj : batch) { - dstPCache->canonicalize(nObj->getHash(), nObj, true); + dstPCache->canonicalize_replace_cache(nObj->getHash(), nObj); dstNCache->erase(nObj->getHash()); storeStats(nObj->getData().size()); } diff --git a/src/ripple/nodestore/impl/DatabaseNodeImp.cpp b/src/ripple/nodestore/impl/DatabaseNodeImp.cpp index 165b826d585..e0879a617cb 100644 --- a/src/ripple/nodestore/impl/DatabaseNodeImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseNodeImp.cpp @@ -29,7 +29,7 @@ DatabaseNodeImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t seq) { auto nObj = NodeObject::createObject(type, std::move(data), hash); - pCache_->canonicalize(hash, nObj, true); + pCache_->canonicalize_replace_cache(hash, nObj); backend_->store(nObj); nCache_->erase(hash); storeStats(nObj->getData().size()); diff --git a/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp b/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp index edd23e62010..00a9365cc73 100644 --- a/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseRotatingImp.cpp @@ -64,7 +64,7 @@ DatabaseRotatingImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t seq) { auto nObj = NodeObject::createObject(type, std::move(data), hash); - pCache_->canonicalize(hash, nObj, true); + pCache_->canonicalize_replace_cache(hash, nObj); getWritableBackend()->store(nObj); nCache_->erase(hash); storeStats(nObj->getData().size()); diff --git a/src/ripple/nodestore/impl/DatabaseShardImp.cpp b/src/ripple/nodestore/impl/DatabaseShardImp.cpp index ae1072b36ae..eaeabc19cef 100644 --- a/src/ripple/nodestore/impl/DatabaseShardImp.cpp +++ b/src/ripple/nodestore/impl/DatabaseShardImp.cpp @@ -892,7 +892,7 @@ DatabaseShardImp::store( auto [backend, pCache, nCache] = shard->getBackendAll(); auto nObj {NodeObject::createObject(type, std::move(data), hash)}; - pCache->canonicalize(hash, nObj, true); + pCache->canonicalize_replace_cache(hash, nObj); backend->store(nObj); nCache->erase(hash); diff --git a/src/ripple/shamap/impl/SHAMap.cpp b/src/ripple/shamap/impl/SHAMap.cpp index 72a8b46e579..3b0817e08fd 100644 --- a/src/ripple/shamap/impl/SHAMap.cpp +++ b/src/ripple/shamap/impl/SHAMap.cpp @@ -1072,7 +1072,7 @@ SHAMap::canonicalize(SHAMapHash const& hash, std::shared_ptr assert (node->getSeq() == 0); assert (node->getNodeHash() == hash); - f_.treecache().canonicalize (hash.as_uint256(), node); + f_.treecache().canonicalize_replace_client(hash.as_uint256(), node); } void diff --git a/src/test/basics/TaggedCache_test.cpp b/src/test/basics/TaggedCache_test.cpp index eee82076678..4480aad1e89 100644 --- a/src/test/basics/TaggedCache_test.cpp +++ b/src/test/basics/TaggedCache_test.cpp @@ -103,7 +103,7 @@ class TaggedCache_test : public beast::unit_test::suite { Cache::mapped_ptr const p1 (c.fetch (3)); Cache::mapped_ptr p2 (std::make_shared ("three")); - c.canonicalize (3, p2); + c.canonicalize_replace_client(3, p2); BEAST_EXPECT(p1.get() == p2.get()); } ++clock; @@ -134,7 +134,7 @@ class TaggedCache_test : public beast::unit_test::suite BEAST_EXPECT(c.getTrackSize() == 1); // Canonicalize a new object with the same key Cache::mapped_ptr p2 (std::make_shared ("four")); - BEAST_EXPECT(c.canonicalize (4, p2, false)); + BEAST_EXPECT(c.canonicalize_replace_client(4, p2)); BEAST_EXPECT(c.getCacheSize() == 1); BEAST_EXPECT(c.getTrackSize() == 1); // Make sure we get the original object From 2b01769b4f22c212b64777704c69577c0f0a8571 Mon Sep 17 00:00:00 2001 From: Howard Hinnant Date: Fri, 20 Mar 2020 11:31:09 -0400 Subject: [PATCH 11/14] Remove all uses of the name scoped_lock * scoped_lock is now a std name with subtly different semantics compared to lock_guard. Namely it can be used to lock 0 or more mutexes. This is valuable, but can also be accidentally used to lock 0 mutexes when 1 was intended, creating a run-time error. Therefore, if and when we use scoped_lock, extra care needs to be taken in reviewing that code to ensure it doesn't accidentally lock 0 mutexes when 1 was intended. To aid in such careful reviewing, the use of the name scoped_lock should be limited to those cases where the number of mutexes is not exactly one. --- src/ripple/core/impl/semaphore.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ripple/core/impl/semaphore.h b/src/ripple/core/impl/semaphore.h index 4b417525166..392a0d48718 100644 --- a/src/ripple/core/impl/semaphore.h +++ b/src/ripple/core/impl/semaphore.h @@ -29,8 +29,6 @@ template class basic_semaphore { private: - using scoped_lock = std::unique_lock ; - Mutex m_mutex; CondVar m_cond; std::size_t m_count; @@ -49,7 +47,7 @@ class basic_semaphore /** Increment the count and unblock one waiting thread. */ void notify () { - scoped_lock lock (m_mutex); + std::lock_guard lock{m_mutex}; ++m_count; m_cond.notify_one (); } @@ -57,7 +55,7 @@ class basic_semaphore /** Block until notify is called. */ void wait () { - scoped_lock lock (m_mutex); + std::unique_lock lock{m_mutex}; while (m_count == 0) m_cond.wait (lock); --m_count; @@ -68,7 +66,7 @@ class basic_semaphore */ bool try_wait () { - scoped_lock lock (m_mutex); + std::lock_guard lock{m_mutex}; if (m_count == 0) return false; --m_count; From f72ed18fa16544f2c0db655aa120834af423edfb Mon Sep 17 00:00:00 2001 From: Howard Hinnant Date: Mon, 2 Mar 2020 17:24:35 -0500 Subject: [PATCH 12/14] Update SHAMap Documentation --- src/ripple/shamap/README.md | 406 +++++++++++++++++++++++++----------- 1 file changed, 283 insertions(+), 123 deletions(-) diff --git a/src/ripple/shamap/README.md b/src/ripple/shamap/README.md index e28d3bbfc31..a04a63c5f52 100644 --- a/src/ripple/shamap/README.md +++ b/src/ripple/shamap/README.md @@ -1,189 +1,349 @@ # SHAMap Introduction # -July 2014 +March 2020 -The SHAMap is a Merkle tree (http://en.wikipedia.org/wiki/Merkle_tree). -The SHAMap is also a radix tree of radix 16 +The `SHAMap` is a Merkle tree (http://en.wikipedia.org/wiki/Merkle_tree). +The `SHAMap` is also a radix trie of radix 16 (http://en.wikipedia.org/wiki/Radix_tree). -*We need some kind of sensible summary of the SHAMap here.* +The Merkle trie data structure is important because subtrees and even the entire +tree can be compared with other trees in O(1) time by simply comparing the hashes. +This makes it very efficient to determine if two `SHAMap`s contain the same set of +transactions or account state modifications. -A given SHAMap always stores only one of three kinds of data: +The radix trie property is helpful in that a key (hash) of a transaction +or account state can be used to navigate the trie. + +A `SHAMap` is a trie with two node types: + +1. SHAMapInnerNode +2. SHAMapTreeNode + +Both of these nodes directly inherit from SHAMapAbstractNode which holds data +common to both of the node types. + +All non-leaf nodes have type SHAMapInnerNode. + +All leaf nodes have type SHAMapTreeNode. + +The root node is always a SHAMapInnerNode. + +A given `SHAMap` always stores only one of three kinds of data: * Transactions with metadata * Transactions without metadata, or * Account states. -So all of the leaf nodes of a particular SHAMap will always have a uniform -type. The inner nodes carry no data other than the hash of the nodes -beneath them. +So all of the leaf nodes of a particular `SHAMap` will always have a uniform type. +The inner nodes carry no data other than the hash of the nodes beneath them. +All nodes are owned by shared_ptrs resident in either other nodes, or in case of +the root node, a shared_ptr in the `SHAMap` itself. The use of shared_ptrs +permits more than one `SHAMap` at a time to share ownership of a node. This +occurs (for example), when a copy of a `SHAMap` is made. -## SHAMap Types ## +Copies are made with the `snapShot` function as opposed to the `SHAMap` copy +constructor. See the section on `SHAMap` creation for more details about +`snapShot`. -There are two different ways of building and using a SHAMap: +Sequence numbers are used to further customize the node ownership strategy. See +the section on sequence numbers for details on sequence numbers. - 1. A mutable SHAMap and - 2. An immutable SHAMap +![node diagram](https://user-images.githubusercontent.com/46455409/77350005-1ef12c80-6cf9-11ea-9c8d-56410f442859.png) -The distinction here is not of the classic C++ immutable-means-unchanging -sense. An immutable SHAMap contains *nodes* that are immutable. Also, -once a node has been located in an immutable SHAMap, that node is -guaranteed to persist in that SHAMap for the lifetime of the SHAMap. +## Mutability ## -So, somewhat counter-intuitively, an immutable SHAMap may grow as new nodes -are introduced. But an immutable SHAMap will never get smaller (until it -entirely evaporates when it is destroyed). Nodes, once introduced to the -immutable SHAMap, also never change their location in memory. So nodes in -an immutable SHAMap can be handled using raw pointers (if you're careful). +There are two different ways of building and using a `SHAMap`: -One consequence of this design is that an immutable SHAMap can never be -"trimmed". There is no way to identify unnecessary nodes in an immutable -SHAMap that could be removed. Once a node has been brought into the -in-memory SHAMap, that node stays in memory for the life of the SHAMap. + 1. A mutable `SHAMap` and + 2. An immutable `SHAMap` -Most SHAMaps are immutable, in the sense that they don't modify or remove -their contained nodes. +The distinction here is not of the classic C++ immutable-means-unchanging sense. + An immutable `SHAMap` contains *nodes* that are immutable. Also, once a node has +been located in an immutable `SHAMap`, that node is guaranteed to persist in that +`SHAMap` for the lifetime of the `SHAMap`. -An example where a mutable SHAMap is required is when we want to apply -transactions to the last closed ledger. To do so we'd make a mutable -snapshot of the state tree and then start applying transactions to it. -Because the snapshot is mutable, changes to nodes in the snapshot will not -affect nodes in other SHAMAps. +So, somewhat counter-intuitively, an immutable `SHAMap` may grow as new nodes are +introduced. But an immutable `SHAMap` will never get smaller (until it entirely +evaporates when it is destroyed). Nodes, once introduced to the immutable +`SHAMap`, also never change their location in memory. So nodes in an immutable +`SHAMap` can be handled using raw pointers (if you're careful). -An example using a immutable ledger would be when there's an open ledger -and some piece of code wishes to query the state of the ledger. In this -case we don't wish to change the state of the SHAMap, so we'd use an -immutable snapshot. +One consequence of this design is that an immutable `SHAMap` can never be +"trimmed". There is no way to identify unnecessary nodes in an immutable `SHAMap` +that could be removed. Once a node has been brought into the in-memory `SHAMap`, +that node stays in memory for the life of the `SHAMap`. +Most `SHAMap`s are immutable, in the sense that they don't modify or remove their +contained nodes. -## SHAMap Creation ## +An example where a mutable `SHAMap` is required is when we want to apply +transactions to the last closed ledger. To do so we'd make a mutable snapshot +of the state trie and then start applying transactions to it. Because the +snapshot is mutable, changes to nodes in the snapshot will not affect nodes in +other `SHAMap`s. + +An example using a immutable ledger would be when there's an open ledger and +some piece of code wishes to query the state of the ledger. In this case we +don't wish to change the state of the `SHAMap`, so we'd use an immutable snapshot. -A SHAMap is usually not created from vacuum. Once an initial SHAMap is -constructed, later SHAMaps are usually created by calling -snapShot(bool isMutable) on the original SHAMap(). The returned SHAMap -has the expected characteristics (mutable or immutable) based on the passed -in flag. +## Sequence numbers ## -It is cheaper to make an immutable snapshot of a SHAMap than to make a mutable -snapshot. If the SHAMap snapshot is mutable then any of the nodes that might -be modified must be copied before they are placed in the mutable map. +Both `SHAMap`s and their nodes carry a sequence number. This is simply an +unsigned number that indicates ownership or membership, or a non-membership. +`SHAMap`s sequence numbers normally start out as 1. However when a snap-shot of +a `SHAMap` is made, the copy's sequence number is 1 greater than the original. -## SHAMap Thread Safety ## +The nodes of a `SHAMap` have their own copy of a sequence number. If the `SHAMap` +is mutable, meaning it can change, then all of its nodes must have the +same sequence number as the `SHAMap` itself. This enforces an invariant that none +of the nodes are shared with other `SHAMap`s. -*This description is obsolete and needs to be rewritten.* +When a `SHAMap` needs to have a private copy of a node, not shared by any other +`SHAMap`, it first clones it and then sets the new copy to have a sequence number +equal to the `SHAMap` sequence number. The `unshareNode` is a private utility +which automates the task of first checking if the node is already sharable, and +if so, cloning it and giving it the proper sequence number. An example case +where a private copy is needed is when an inner node needs to have a child +pointer altered. Any modification to a node will require a non-shared node. -SHAMaps can be thread safe, depending on how they are used. The SHAMap -uses a SyncUnorderedMap for its storage. The SyncUnorderedMap has three -thread-safe methods: +When a `SHAMap` decides that it is safe to share a node of its own, it sets the +node's sequence number to 0 (a `SHAMap` never has a sequence number of 0). This +is done for every node in the trie when `SHAMap::walkSubTree` is executed. - * size(), - * canonicalize(), and - * retrieve() +Note that other objects in rippled also have sequence numbers (e.g. ledgers). +The `SHAMap` and node sequence numbers should not be confused with these other +sequence numbers (no relation). -As long as the SHAMap uses only those three interfaces to its storage -(the mTNByID variable [which stands for Tree Node by ID]) the SHAMap is -thread safe. +## SHAMap Creation ## +A `SHAMap` is usually not created from vacuum. Once an initial `SHAMap` is +constructed, later `SHAMap`s are usually created by calling snapShot(bool +isMutable) on the original `SHAMap`. The returned `SHAMap` has the expected +characteristics (mutable or immutable) based on the passed in flag. + +It is cheaper to make an immutable snapshot of a `SHAMap` than to make a mutable +snapshot. If the `SHAMap` snapshot is mutable then sharable nodes must be +copied before they are placed in the mutable map. + +A new `SHAMap` is created with each new ledger round. Transactions not executed +in the previous ledger populate the `SHAMap` for the new ledger. + +## Storing SHAMap data in the database ## + +When consensus is reached, the ledger is closed. As part of this process, the +`SHAMap` is stored to the database by calling `SHAMap::flushDirty`. + +Both `unshare()` and `flushDirty` walk the `SHAMap` by calling +`SHAMap::walkSubTree`. As `unshare()` walks the trie, nodes are not written to +the database, and as `flushDirty` walks the trie nodes are written to the +database. `walkSubTree` visits every node in the trie. This process must ensure +that each node is only owned by this trie, and so "unshares" as it walks each +node (from the root down). This is done in the `preFlushNode` function by +ensuring that the node has a sequence number equal to that of the `SHAMap`. If +the node doesn't, it is cloned. + +For each inner node encountered (starting with the root node), each of the +children are inspected (from 1 to 16). For each child, if it has a non-zero +sequence number (unshareable), the child is first copied. Then if the child is +an inner node, we recurse down to that node's children. Otherwise we've found a +leaf node and that node is written to the database. A count of each leaf node +that is visited is kept. The hash of the data in the leaf node is computed at +this time, and the child is reassigned back into the parent inner node just in +case the COW operation created a new pointer to this leaf node. + +After processing each node, the node is then marked as sharable again by setting +its sequence number to 0. + +After all of an inner node's children are processed, then its hash is updated +and the inner node is written to the database. Then this inner node is assigned +back into it's parent node, again in case the COW operation created a new +pointer to it. ## Walking a SHAMap ## -*We need a good description of why someone would walk a SHAMap and* -*how it works in the code* - +The private function `SHAMap::walkTowardsKey` is a good example of *how* to walk +a `SHAMap`, and the various functions that call `walkTowardsKey` are good examples +of *why* one would want to walk a `SHAMap` (e.g. `SHAMap::findKey`). +`walkTowardsKey` always starts at the root of the `SHAMap` and traverses down +through the inner nodes, looking for a leaf node along a path in the trie +designated by a `uint256`. + +As one walks the trie, one can *optionally* keep a stack of nodes that one has +passed through. This isn't necessary for walking the trie, but many clients +will use the stack after finding the desired node. For example if one is +deleting a node from the trie, the stack is handy for repairing invariants in +the trie after the deletion. + +To assist in walking the trie, `SHAMap::walkTowardsKey` uses a `SHAMapNodeID` +that identifies a node by its path from the root and its depth in the trie. The +path is just a "list" of numbers, each in the range [0 .. 15], depicting which +child was chosen at each node starting from the root. Each choice is represented +by 4 bits, and then packed in sequence into a `uint256` (such that the longest +path possible has 256 / 4 = 64 steps). The high 4 bits of the first byte +identify which child of the root is chosen, the lower 4 bits of the first byte +identify the child of that node, and so on. The `SHAMapNodeID` identifying the +root node has an ID of 0 and a depth of 0. See `SHAMapNodeID::selectBranch` for +details of how a `SHAMapNodeID` selects a "branch" (child) by indexing into its +path with its depth. + +While the current node is an inner node, traversing down the trie from the root +continues, unless the path indicates a child that does not exist. And in this +case, `nullptr` is returned to indicate no leaf node along the given path +exists. Otherwise a leaf node is found and a (non-owning) pointer to it is +returned. At each step, if a stack is requested, a +`pair, SHAMapNodeID>` is pushed onto the stack. + +When a child node is found by `selectBranch`, the traversal to that node +consists of two steps: + +1. Update the `shared_ptr` to the current node. +2. Update the `SHAMapNodeID`. + +The first step consists of several attempts to find the node in various places: + +1. In the trie itself. +2. In the node cache. +3. In the database. + +If the node is not found in the trie, then it is installed into the trie as part +of the traversal process. ## Late-arriving Nodes ## -As we noted earlier, SHAMaps (even immutable ones) may grow. If a SHAMap -is searching for a node and runs into an empty spot in the tree, then the -SHAMap looks to see if the node exists but has not yet been made part of -the map. This operation is performed in the `SHAMap::fetchNodeExternalNT()` -method. The *NT* is this case stands for 'No Throw'. +As we noted earlier, `SHAMap`s (even immutable ones) may grow. If a `SHAMap` is +searching for a node and runs into an empty spot in the trie, then the `SHAMap` +looks to see if the node exists but has not yet been made part of the map. This +operation is performed in the `SHAMap::fetchNodeNT()` method. The *NT* +is this case stands for 'No Throw'. -The `fetchNodeExternalNT()` method goes through three phases: +The `fetchNodeNT()` method goes through three phases: 1. By calling `getCache()` we attempt to locate the missing node in the - TreeNodeCache. The TreeNodeCache is a cache of immutable - SHAMapTreeNodes that are shared across all SHAMaps. + TreeNodeCache. The TreeNodeCache is a cache of immutable SHAMapTreeNodes + that are shared across all `SHAMap`s. - Any SHAMapTreeNode that is immutable has a sequence number of zero. - When a mutable SHAMap is created then its SHAMapTreeNodes are given - non-zero sequence numbers. So the `assert (ret->getSeq() == 0)` - simply confirms that the TreeNodeCache indeed gave us an immutable node. + Any SHAMapTreeNode that is immutable has a sequence number of zero + (sharable). When a mutable `SHAMap` is created then its SHAMapTreeNodes are + given non-zero sequence numbers (unsharable). But all nodes in the + TreeNodeCache are immutable, so if one is found here, its sequence number + will be 0. 2. If the node is not in the TreeNodeCache, we attempt to locate the node - in the historic data stored by the data base. The call to - to `fetch(hash)` does that work for us. + in the historic data stored by the data base. The call to to + `fetchNodeFromDB(hash)` does that work for us. - 3. Finally, if ledgerSeq_ is non-zero and we did't locate the node in the - historic data, then we call a MissingNodeHandler. + 3. Finally if a filter exists, we check if it can supply the node. This is + typically the LedgerMaster which tracks the current ledger and ledgers + in the process of closing. - The non-zero ledgerSeq_ indicates that the SHAMap is a complete map that - belongs to a historic ledger with the given (non-zero) sequence number. - So, if all expected data is always present, the MissingNodeHandler should - never be executed. +## Canonicalize ## - And, since we now know that this SHAMap does not fully represent - the data from that ledger, we set the SHAMap's sequence number to zero. +`canonicalize()` is called every time a node is introduced into the `SHAMap`. -If phase 1 returned a node, then we already know that the node is immutable. -However, if either phase 2 executes successfully, then we need to turn the -returned node into an immutable node. That's handled by the call to -`make_shared` inside the try block. That code is inside -a try block because the `fetchNodeExternalNT` method promises not to throw. -In case the constructor called by `make_shared` throws we don't want to -break our promise. +A call to `canonicalize()` stores the node in the `TreeNodeCache` if it does not +already exist in the `TreeNodeCache`. +The calls to `canonicalize()` make sure that if the resulting node is already in +the `SHAMap`, node `TreeNodeCache` or database, then we don't create duplicates +by favoring the copy already in the `TreeNodeCache`. -## Canonicalize ## +By using `canonicalize()` we manage a thread race condition where two different +threads might both recognize the lack of a SHAMapTreeNode at the same time +(during a fetch). If they both attempt to insert the node into the `SHAMap`, then +`canonicalize` makes sure that the first node in wins and the slower thread +receives back a pointer to the node inserted by the faster thread. Recall +that these two `SHAMap`s will share the same `TreeNodeCache`. + +## TreeNodeCache ## + +The `TreeNodeCache` is a `std::unordered_map` keyed on the hash of the +`SHAMap` node. The stored type consists of `shared_ptr`, +`weak_ptr`, and a time point indicating the most recent +access of this node in the cache. The time point is based on +`std::chrono::steady_clock`. + +The container uses a cryptographically secure hash that is randomly seeded. + +The `TreeNodeCache` also carries with it various data used for statistics +and logging, and a target age for the contained nodes. When the target age +for a node is exceeded, and there are no more references to the node, the +node is removed from the `TreeNodeCache`. + +## FullBelowCache ## + +This cache remembers which trie keys have all of their children resident in a +`SHAMap`. This optimizes the process of acquiring a complete trie. This is used +when creating the missing nodes list. Missing nodes are those nodes that a +`SHAMap` refers to but that are not stored in the local database. + +As a depth-first walk of a `SHAMap` is performed, if an inner node answers true to +`isFullBelow()` then it is known that none of this node's children are missing +nodes, and thus that subtree does not need to be walked. These nodes are stored +in the FullBelowCache. Subsequent walks check the FullBelowCache first when +encountering a node, and ignore that subtree if found. + +## SHAMapAbstractNode ## + +This is a base class for the two concrete node types. It holds the following +common data: + +1. A node type, one of: + a. error + b. inner + c. transaction with no metadata + d. transaction with metadata + e. account state +2. A hash +3. A sequence number + + +## SHAMapInnerNode ## + +SHAMapInnerNode publicly inherits directly from SHAMapAbstractNode. It holds +the following data: + +1. Up to 16 child nodes, each held with a shared_ptr. +2. A hash for each child. +3. A 16-bit bitset with a 1 bit set for each child that exists. +4. Flag to aid online delete and consistency with data on disk. + +## SHAMapTreeNode ## -The calls to `canonicalize()` make sure that if the resulting node is already -in the SHAMap, then we return the node that's already present -- we never -replace a pre-existing node. By using `canonicalize()` we manage a thread -race condition where two different threads might both recognize the lack of a -SHAMapTreeNode at the same time. If they both attempt to insert the node -then `canonicalize` makes sure that the first node in wins and the slower -thread receives back a pointer to the node inserted by the faster thread. +SHAMapTreeNode publicly inherits directly from SHAMapAbstractNode. It holds the +following data: -There's a problem with the current SHAMap design that `canonicalize()` -accommodates. Two different trees can have the exact same node (the same -hash value) with two different IDs. If the TreeNodeCache returns a node -with the same hash but a different ID, then we assume that the ID of the -passed-in node is 'better' than the older ID in the TreeNodeCache. So we -construct a new SHAMapTreeNode by copying the one we found in the -TreeNodeCache, but we give the new node the new ID. Then we replace the -SHAMapTreeNode in the TreeNodeCache with this newly constructed node. +1. A shared_ptr to a const SHAMapItem. -The TreeNodeCache is not subject to the rule that any node must be -resident forever. So it's okay to replace the old node with the new node. +## SHAMapItem ## -The `SHAMap::getCache()` method exhibits the same behavior. +This holds the following data: +1. uint256. The hash of the data. +2. vector. The data (transactions, account info). ## SHAMap Improvements ## -Here's a simple one: the SHAMapTreeNode::mAccessSeq member is currently not -used and could be removed. +Here's a simple one: the SHAMapTreeNode::mAccessSeq member is currently not used +and could be removed. -Here's a more important change. The tree structure is currently embedded -in the SHAMapTreeNodes themselves. It doesn't have to be that way, and -that should be fixed. +Here's a more important change. The trie structure is currently embedded in the +SHAMapTreeNodes themselves. It doesn't have to be that way, and that should be +fixed. -When we navigate the tree (say, like `SHAMap::walkTo()`) we currently -ask each node for information that we could determine locally. We know -the depth because we know how many nodes we have traversed. We know the -ID that we need because that's how we're steering. So we don't need to -store the ID in the node. The next refactor should remove all calls to -`SHAMapTreeNode::GetID()`. +When we navigate the trie (say, like `SHAMap::walkTo()`) we currently ask each +node for information that we could determine locally. We know the depth because +we know how many nodes we have traversed. We know the ID that we need because +that's how we're steering. So we don't need to store the ID in the node. The +next refactor should remove all calls to `SHAMapTreeNode::GetID()`. Then we can remove the NodeID member from SHAMapTreeNode. -Then we can change the SHAMap::mTNBtID member to be mTNByHash. +Then we can change the `SHAMap::mTNBtID` member to be `mTNByHash`. An additional possible refactor would be to have a base type, SHAMapTreeNode, -and derive from that InnerNode and LeafNode types. That would remove -some storage (the array of 16 hashes) from the LeafNodes. That refactor -would also have the effect of simplifying methods like `isLeaf()` and -`hasItem()`. +and derive from that InnerNode and LeafNode types. That would remove some +storage (the array of 16 hashes) from the LeafNodes. That refactor would also +have the effect of simplifying methods like `isLeaf()` and `hasItem()`. From 6123ca3e1d30478cf7d12e360074c91d14c4bf74 Mon Sep 17 00:00:00 2001 From: manojsdoshi Date: Mon, 6 Apr 2020 14:49:51 -0700 Subject: [PATCH 13/14] Hosting the signers list public keys to a new location --- Builds/containers/gitlab-ci/pkgbuild.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Builds/containers/gitlab-ci/pkgbuild.yml b/Builds/containers/gitlab-ci/pkgbuild.yml index 555b9d00333..c431ee665b7 100644 --- a/Builds/containers/gitlab-ci/pkgbuild.yml +++ b/Builds/containers/gitlab-ci/pkgbuild.yml @@ -19,7 +19,7 @@ variables: DPKG_CONTAINER_FULLNAME: "${DPKG_CONTAINER_NAME}:${DPKG_CONTAINER_TAG}" ARTIFACTORY_HOST: "artifactory.ops.ripple.com" ARTIFACTORY_HUB: "${ARTIFACTORY_HOST}:6555" - GIT_SIGN_PUBKEYS_URL: "https://gitlab.ops.ripple.com/snippets/11/raw" + GIT_SIGN_PUBKEYS_URL: "https://gitlab.ops.ripple.com/xrpledger/rippled-packages/snippets/49/raw" PUBLIC_REPO_ROOT: "https://repos.ripple.com/repos" # also need to define this variable ONLY for the primary # build/publish pipeline on the mainline repo: From 3d952f4b2e295059d6ee0982f20b52b1c3e4064e Mon Sep 17 00:00:00 2001 From: manojsdoshi Date: Mon, 6 Apr 2020 17:31:52 -0700 Subject: [PATCH 14/14] Setting version to 1.6.0-b1 --- src/ripple/protocol/impl/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ripple/protocol/impl/BuildInfo.cpp b/src/ripple/protocol/impl/BuildInfo.cpp index 4872671e8d3..57f0e401341 100644 --- a/src/ripple/protocol/impl/BuildInfo.cpp +++ b/src/ripple/protocol/impl/BuildInfo.cpp @@ -31,7 +31,7 @@ namespace BuildInfo { // The build version number. You must edit this for each release // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ -char const* const versionString = "1.5.0" +char const* const versionString = "1.6.0-b1" #if defined(DEBUG) || defined(SANITIZER) "+"