diff --git a/cfg/rippled-example.cfg b/cfg/rippled-example.cfg index c4917d65044..b2b3c03f346 100644 --- a/cfg/rippled-example.cfg +++ b/cfg/rippled-example.cfg @@ -36,7 +36,7 @@ # For more information on where the rippled server instance searches for the # file, visit: # -# https://developers.ripple.com/commandline-usage.html#generic-options +# https://xrpl.org/commandline-usage.html#generic-options # # This file should be named rippled.cfg. This file is UTF-8 with DOS, UNIX, # or Mac style end of lines. Blank lines and lines beginning with '#' are @@ -869,18 +869,65 @@ # # These keys are possible for any type of backend: # +# earliest_seq The default is 32570 to match the XRP ledger +# network's earliest allowed sequence. Alternate +# networks may set this value. Minimum value of 1. +# If a [shard_db] section is defined, and this +# value is present either [node_db] or [shard_db], +# it must be defined with the same value in both +# sections. +# # online_delete Minimum value of 256. Enable automatic purging # of older ledger information. Maintain at least this # number of ledger records online. Must be greater # than or equal to ledger_history. # -# advisory_delete 0 for disabled, 1 for enabled. If set, then -# require administrative RPC call "can_delete" -# to enable online deletion of ledger records. +# These keys modify the behavior of online_delete, and thus are only +# relevant if online_delete is defined and non-zero: # -# earliest_seq The default is 32570 to match the XRP ledger -# network's earliest allowed sequence. Alternate -# networks may set this value. Minimum value of 1. +# advisory_delete 0 for disabled, 1 for enabled. If set, the +# administrative RPC call "can_delete" is required +# to enable online deletion of ledger records. +# Online deletion does not run automatically if +# non-zero and the last deletion was on a ledger +# greater than the current "can_delete" setting. +# Default is 0. +# +# delete_batch When automatically purging, SQLite database +# records are deleted in batches. This value +# controls the maximum size of each batch. Larger +# batches keep the databases locked for more time, +# which may cause other functions to fall behind, +# and thus cause the node to lose sync. +# Default is 100. +# +# back_off_milliseconds +# Number of milliseconds to wait between +# online_delete batches to allow other functions +# to catch up. +# Default is 100. +# +# age_threshold_seconds +# The online delete process will only run if the +# latest validated ledger is younger than this +# number of seconds. +# Default is 60. +# +# recovery_wait_seconds +# The online delete process checks periodically +# that rippled is still in sync with the network, +# and that the validated ledger is less than +# 'age_threshold_seconds' old. By default, if it +# is not the online delete process aborts and +# tries again later. If 'recovery_wait_seconds' +# is set and rippled is out of sync, but likely to +# recover quickly, then online delete will wait +# this number of seconds for rippled to get back +# into sync before it aborts. +# Set this value if the node is otherwise staying +# in sync, or recovering quickly, but the online +# delete process is unable to finish. +# Default is unset. # # Notes: # The 'node_db' entry configures the primary, persistent storage. @@ -892,6 +939,12 @@ # [import_db] Settings for performing a one-time import (optional) # [database_path] Path to the book-keeping databases. # +# The server creates and maintains 4 to 5 bookkeeping SQLite databases in +# the 'database_path' location. If you omit this configuration setting, +# the server creates a directory called "db" located in the same place as +# your rippled.cfg file. +# Partial pathnames are relative to the location of the rippled executable. +# # [shard_db] Settings for the Shard Database (optional) # # Format (without spaces): @@ -907,12 +960,84 @@ # # max_size_gb Maximum disk space the database will utilize (in gigabytes) # +# [sqlite] Tuning settings for the SQLite databases (optional) +# +# Format (without spaces): +# One or more lines of case-insensitive key / value pairs: +# '=' +# ... +# +# Example 1: +# sync_level=low +# +# Example 2: +# journal_mode=off +# synchronous=off +# +# WARNING: These settings can have significant effects on data integrity, +# particularly in systemic failure scenarios. It is strongly recommended +# that they be left at their defaults unless the server is having +# performance issues during normal operation or during automatic purging +# (online_delete) operations. A warning will be logged on startup if +# 'ledger_history' is configured to store more than 10,000,000 ledgers and +# any of these settings are less safe than the default. This is due to the +# inordinate amount of time and bandwidth it will take to safely rebuild a +# corrupted database of that size from other peers. +# +# Optional keys: # -# There are 4 bookkeeping SQLite database that the server creates and -# maintains. If you omit this configuration setting, it will default to -# creating a directory called "db" located in the same place as your -# rippled.cfg file. Partial pathnames will be considered relative to -# the location of the rippled executable. +# safety_level Valid values: high, low +# The default is "high", which tunes the SQLite +# databases in the most reliable mode, and is +# equivalent to: +# journal_mode=wal +# synchronous=normal +# temp_store=file +# "low" is equivalent to: +# journal_mode=memory +# synchronous=off +# temp_store=memory +# These "low" settings trade speed and reduced I/O +# for a higher risk of data loss. See the +# individual settings below for more information. +# This setting may not be combined with any of the +# other tuning settings: "journal_mode", +# "synchronous", or "temp_store". +# +# journal_mode Valid values: delete, truncate, persist, memory, wal, off +# The default is "wal", which uses a write-ahead +# log to implement database transactions. +# Alternately, "memory" saves disk I/O, but if +# rippled crashes during a transaction, the +# database is likely to be corrupted. +# See https://www.sqlite.org/pragma.html#pragma_journal_mode +# for more details about the available options. +# This setting may not be combined with the +# "safety_level" setting. +# +# synchronous Valid values: off, normal, full, extra +# The default is "normal", which works well with +# the "wal" journal mode. Alternatively, "off" +# allows rippled to continue as soon as data is +# passed to the OS, which can significantly +# increase speed, but risks data corruption if +# the host computer crashes before writing that +# data to disk. +# See https://www.sqlite.org/pragma.html#pragma_synchronous +# for more details about the available options. +# This setting may not be combined with the +# "safety_level" setting. +# +# temp_store Valid values: default, file, memory +# The default is "file", which will use files +# for temporary database tables and indices. +# Alternatively, "memory" may save I/O, but +# rippled does not currently use many, if any, +# of these temporary objects. +# See https://www.sqlite.org/pragma.html#pragma_temp_store +# for more details about the available options. +# This setting may not be combined with the +# "safety_level" setting. # # # @@ -1212,24 +1337,27 @@ medium # This is primary persistent datastore for rippled. This includes transaction # metadata, account states, and ledger headers. Helpful information can be -# found here: https://ripple.com/wiki/NodeBackEnd -# delete old ledgers while maintaining at least 2000. Do not require an -# external administrative command to initiate deletion. +# found at https://xrpl.org/capacity-planning.html#node-db-type +# type=NuDB is recommended for non-validators with fast SSDs. Validators or +# slow / spinning disks should use RocksDB. Caution: Spinning disks are +# not recommended. They do not perform well enough to consistently remain +# synced to the network. +# online_delete=512 is recommended to delete old ledgers while maintaining at +# least 512. +# advisory_delete=0 allows the online delete process to run automatically +# when the node has approximately two times the "online_delete" value of +# ledgers. No external administrative command is required to initiate +# deletion. [node_db] -type=RocksDB -path=/var/lib/rippled/db/rocksdb -open_files=2000 -filter_bits=12 -cache_mb=256 -file_size_mb=8 -file_size_mult=2 -online_delete=2000 +type=NuDB +path=/var/lib/rippled/db/nudb +online_delete=512 advisory_delete=0 # 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 +# NuDB requires SSD storage. Helpful information can be found at +# https://xrpl.org/history-sharding.html #[shard_db] #path=/var/lib/rippled/db/shards/nudb #max_size_gb=500 @@ -1248,7 +1376,8 @@ time.apple.com time.nist.gov pool.ntp.org -# To use the XRP test network (see https://ripple.com/build/xrp-test-net/), +# To use the XRP test network +# (see https://xrpl.org/connect-your-rippled-to-the-xrp-test-net.html), # use the following [ips] section: # [ips] # r.altnet.rippletest.net 51235 diff --git a/src/ripple/app/ledger/Ledger.cpp b/src/ripple/app/ledger/Ledger.cpp index 3a6c43376c5..b583b540633 100644 --- a/src/ripple/app/ledger/Ledger.cpp +++ b/src/ripple/app/ledger/Ledger.cpp @@ -228,14 +228,14 @@ Ledger::Ledger( !txMap_->fetchRoot(SHAMapHash{info_.txHash}, nullptr)) { loaded = false; - JLOG(j.warn()) << "Don't have TX root for ledger"; + JLOG(j.warn()) << "Don't have transaction root for ledger" << info_.seq; } if (info_.accountHash.isNonZero() && !stateMap_->fetchRoot(SHAMapHash{info_.accountHash}, nullptr)) { loaded = false; - JLOG(j.warn()) << "Don't have AS root for ledger"; + JLOG(j.warn()) << "Don't have state data root for ledger" << info_.seq; } txMap_->setImmutable(); diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index b82fce0bd12..8b1b288a7e6 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -54,6 +54,10 @@ class Transaction; class LedgerMaster : public Stoppable, public AbstractFetchPackContainer { public: + // Age for last validated ledger if the process has yet to validate. + static constexpr std::chrono::seconds NO_VALIDATED_LEDGER_AGE = + std::chrono::hours{24 * 14}; + explicit LedgerMaster( Application& app, Stopwatch& stopwatch, diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 9a8f7dfe382..6fb71a483f4 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -269,7 +269,7 @@ LedgerMaster::getValidatedLedgerAge() if (valClose == 0s) { JLOG(m_journal.debug()) << "No validated ledger"; - return weeks{2}; + return NO_VALIDATED_LEDGER_AGE; } std::chrono::seconds ret = app_.timeKeeper().closeTime().time_since_epoch(); diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index 3b37c736963..0db9051f5e6 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -1022,7 +1022,7 @@ class ApplicationImp : public Application, public RootStoppable, public BasicApp try { - auto const setup = setup_DatabaseCon(*config_); + auto setup = setup_DatabaseCon(*config_, m_journal); // transaction database mTxnDB = std::make_unique( @@ -1072,6 +1072,7 @@ class ApplicationImp : public Application, public RootStoppable, public BasicApp mLedgerDB->setupCheckpointing(m_jobQueue.get(), logs()); // wallet database + setup.useGlobalPragma = false; mWalletDB = std::make_unique( setup, WalletDBName, @@ -1363,7 +1364,7 @@ class ApplicationImp : public Application, public RootStoppable, public BasicApp JLOG(m_journal.fatal()) << "Free SQLite space for transaction db is less than " "512MB. To fix this, rippled must be executed with the " - "vacuum parameter before restarting. " + "\"--vacuum\" parameter before restarting. " "Note that this activity can take multiple days, " "depending on database size."; signalStop(); diff --git a/src/ripple/app/main/DBInit.h b/src/ripple/app/main/DBInit.h index 2aa183d4e70..0a561be8834 100644 --- a/src/ripple/app/main/DBInit.h +++ b/src/ripple/app/main/DBInit.h @@ -26,13 +26,23 @@ namespace ripple { //////////////////////////////////////////////////////////////////////////////// +// These pragmas are built at startup and applied to all database +// connections, unless otherwise noted. +inline constexpr char const* CommonDBPragmaJournal{"PRAGMA journal_mode=%s;"}; +inline constexpr char const* CommonDBPragmaSync{"PRAGMA synchronous=%s;"}; +inline constexpr char const* CommonDBPragmaTemp{"PRAGMA temp_store=%s;"}; +// A warning will be logged if any lower-safety sqlite tuning settings +// are used and at least this much ledger history is configured. This +// includes full history nodes. This is because such a large amount of +// data will be more difficult to recover if a rare failure occurs, +// which are more likely with some of the other available tuning settings. +inline constexpr std::uint32_t SQLITE_TUNING_CUTOFF = 10'000'000; + // Ledger database holds ledgers and ledger confirmations inline constexpr auto LgrDBName{"ledger.db"}; -inline constexpr std::array LgrDBPragma{ - {"PRAGMA synchronous=NORMAL;", - "PRAGMA journal_mode=WAL;", - "PRAGMA journal_size_limit=1582080;"}}; +inline constexpr std::array LgrDBPragma{ + {"PRAGMA journal_size_limit=1582080;"}}; inline constexpr std::array LgrDBInit{ {"BEGIN TRANSACTION;", @@ -61,22 +71,13 @@ inline constexpr std::array LgrDBInit{ // Transaction database holds transactions and public keys inline constexpr auto TxDBName{"transaction.db"}; -inline constexpr -#if (ULONG_MAX > UINT_MAX) && !defined(NO_SQLITE_MMAP) - std::array - TxDBPragma +inline constexpr std::array TxDBPragma { - { -#else - std::array TxDBPragma {{ -#endif - "PRAGMA page_size=4096;", "PRAGMA synchronous=NORMAL;", - "PRAGMA journal_mode=WAL;", "PRAGMA journal_size_limit=1582080;", - "PRAGMA max_page_count=2147483646;", + "PRAGMA page_size=4096;", "PRAGMA journal_size_limit=1582080;", + "PRAGMA max_page_count=2147483646;", #if (ULONG_MAX > UINT_MAX) && !defined(NO_SQLITE_MMAP) - "PRAGMA mmap_size=17179869184;" + "PRAGMA mmap_size=17179869184;" #endif - } }; inline constexpr std::array TxDBInit{ @@ -115,10 +116,8 @@ inline constexpr 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 AcquireShardDBPragma{ + {"PRAGMA journal_size_limit=1582080;"}}; inline constexpr std::array AcquireShardDBInit{ {"CREATE TABLE IF NOT EXISTS Shard ( \ @@ -130,6 +129,7 @@ inline constexpr std::array AcquireShardDBInit{ //////////////////////////////////////////////////////////////////////////////// // Pragma for Ledger and Transaction databases with complete shards +// These override the CommonDBPragma values defined above. inline constexpr std::array CompleteShardDBPragma{ {"PRAGMA synchronous=OFF;", "PRAGMA journal_mode=OFF;"}}; @@ -172,6 +172,7 @@ inline constexpr std::array WalletDBInit{ static constexpr auto stateDBName{"state.db"}; +// These override the CommonDBPragma values defined above. static constexpr std::array DownloaderDBPragma{ {"PRAGMA synchronous=FULL;", "PRAGMA journal_mode=DELETE;"}}; diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index ccc3f2c773e..e8ed917587f 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -354,10 +354,7 @@ run(int argc, char** argv) "nodetoshard", "Import node store into shards")( "replay", "Replay a ledger close.")( "start", "Start from a fresh Ledger.")( - "vacuum", - po::value(), - "VACUUM the transaction db. Mandatory string argument specifies " - "temporary directory path.")( + "vacuum", "VACUUM the transaction db.")( "valid", "Consider the initial ledger a valid network ledger.")( "validateShards", shardsText.c_str()); @@ -520,24 +517,22 @@ run(int argc, char** argv) } using namespace boost::filesystem; - DatabaseCon::Setup dbSetup = setup_DatabaseCon(*config); + DatabaseCon::Setup const dbSetup = setup_DatabaseCon(*config); path dbPath = dbSetup.dataDir / TxDBName; - path tmpPath = vm["vacuum"].as(); try { uintmax_t const dbSize = file_size(dbPath); assert(dbSize != static_cast(-1)); - if (space(tmpPath).available < dbSize) + if (auto available = space(dbPath.parent_path()).available; + available < dbSize) { - std::cerr << "A valid directory for vacuuming must be " - "specified on a filesystem with at least " - "as much free space as the size of " + std::cerr << "The database filesystem must have at least as " + "much free space as the size of " << dbPath.string() << ", which is " << dbSize - << " bytes. The filesystem for " << tmpPath.string() - << " only has " << space(tmpPath).available - << " bytes.\n"; + << " bytes. Only " << available + << " bytes are available.\n"; return -1; } @@ -546,16 +541,19 @@ run(int argc, char** argv) auto& session = txnDB->getSession(); std::uint32_t pageSize; + // Only the most trivial databases will fit in memory on typical + // (recommended) software. Force temp files to be written to disk + // regardless of the config settings. + session << boost::format(CommonDBPragmaTemp) % "file"; session << "PRAGMA page_size;", soci::into(pageSize); std::cout << "VACUUM beginning. page_size: " << pageSize << std::endl; - session << "PRAGMA journal_mode=OFF;"; - session << "PRAGMA temp_store_directory=\"" << tmpPath.string() - << "\";"; session << "VACUUM;"; - session << "PRAGMA journal_mode=WAL;"; + assert(dbSetup.globalPragma); + for (auto const& p : *dbSetup.globalPragma) + session << p; session << "PRAGMA page_size;", soci::into(pageSize); std::cout << "VACUUM finished. page_size: " << pageSize diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 66d01b791e0..0dc0771d244 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -2757,16 +2757,24 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) if (std::abs(closeOffset.count()) >= 60) l[jss::close_time_offset] = closeOffset.count(); - auto lCloseTime = lpClosed->info().closeTime; - auto closeTime = app_.timeKeeper().closeTime(); - if (lCloseTime <= closeTime) + constexpr std::chrono::seconds highAgeThreshold{1000000}; + if (m_ledgerMaster.haveValidated()) { - using namespace std::chrono_literals; - auto age = closeTime - lCloseTime; - if (age < 1000000s) - l[jss::age] = Json::UInt(age.count()); - else - l[jss::age] = 0; + auto const age = m_ledgerMaster.getValidatedLedgerAge(); + l[jss::age] = + Json::UInt(age < highAgeThreshold ? age.count() : 0); + } + else + { + auto lCloseTime = lpClosed->info().closeTime; + auto closeTime = app_.timeKeeper().closeTime(); + if (lCloseTime <= closeTime) + { + using namespace std::chrono_literals; + auto age = closeTime - lCloseTime; + l[jss::age] = + Json::UInt(age < highAgeThreshold ? age.count() : 0); + } } } diff --git a/src/ripple/app/misc/SHAMapStoreImp.cpp b/src/ripple/app/misc/SHAMapStoreImp.cpp index 94deae5d276..590decf1924 100644 --- a/src/ripple/app/misc/SHAMapStoreImp.cpp +++ b/src/ripple/app/misc/SHAMapStoreImp.cpp @@ -180,13 +180,24 @@ SHAMapStoreImp::SHAMapStoreImp( section.set("filter_bits", "10"); } - get_if_exists(section, "delete_batch", deleteBatch_); - get_if_exists(section, "backOff", backOff_); - get_if_exists(section, "age_threshold", ageThreshold_); get_if_exists(section, "online_delete", deleteInterval_); if (deleteInterval_) { + // Configuration that affects the behavior of online delete + get_if_exists(section, "delete_batch", deleteBatch_); + std::uint32_t temp; + if (get_if_exists(section, "back_off_milliseconds", temp) || + // Included for backward compaibility with an undocumented setting + get_if_exists(section, "backOff", temp)) + { + backOff_ = std::chrono::milliseconds{temp}; + } + if (get_if_exists(section, "age_threshold_seconds", temp)) + ageThreshold_ = std::chrono::seconds{temp}; + if (get_if_exists(section, "recovery_wait_seconds", temp)) + recoveryWaitTime_.emplace(std::chrono::seconds{temp}); + get_if_exists(section, "advisory_delete", advisoryDelete_); auto const minInterval = config.standalone() @@ -348,23 +359,14 @@ SHAMapStoreImp::run() // will delete up to (not including) lastRotated if (validatedSeq >= lastRotated + deleteInterval_ && - canDelete_ >= lastRotated - 1) + canDelete_ >= lastRotated - 1 && !health()) { JLOG(journal_.warn()) << "rotating validatedSeq " << validatedSeq << " lastRotated " << lastRotated << " deleteInterval " << deleteInterval_ - << " canDelete_ " << canDelete_; - - switch (health()) - { - case Health::stopping: - stopped(); - return; - case Health::unhealthy: - continue; - case Health::ok: - default:; - } + << " canDelete_ " << canDelete_ << " state " + << app_.getOPs().strOperatingMode(false) << " age " + << ledgerMaster_->getValidatedLedgerAge().count() << 's'; clearPrior(lastRotated); switch (health()) @@ -378,14 +380,13 @@ SHAMapStoreImp::run() default:; } + JLOG(journal_.debug()) << "copying ledger " << validatedSeq; std::uint64_t nodeCount = 0; validatedLedger->stateMap().snapShot(false)->visitNodes(std::bind( &SHAMapStoreImp::copyNode, this, std::ref(nodeCount), std::placeholders::_1)); - JLOG(journal_.debug()) << "copied ledger " << validatedSeq - << " nodecount " << nodeCount; switch (health()) { case Health::stopping: @@ -396,9 +397,12 @@ SHAMapStoreImp::run() case Health::ok: default:; } + // Only log if we completed without a "health" abort + JLOG(journal_.debug()) << "copied ledger " << validatedSeq + << " nodecount " << nodeCount; + JLOG(journal_.debug()) << "freshening caches"; freshenCaches(); - JLOG(journal_.debug()) << validatedSeq << " freshened caches"; switch (health()) { case Health::stopping: @@ -409,7 +413,10 @@ SHAMapStoreImp::run() case Health::ok: default:; } + // Only log if we completed without a "health" abort + JLOG(journal_.debug()) << validatedSeq << " freshened caches"; + JLOG(journal_.trace()) << "Making a new backend"; auto newBackend = makeBackendRotating(); JLOG(journal_.debug()) << validatedSeq << " new backend " << newBackend->getName(); @@ -559,26 +566,38 @@ SHAMapStoreImp::makeBackendRotating(std::string path) return backend; } -bool +void SHAMapStoreImp::clearSql( DatabaseCon& database, LedgerIndex lastRotated, std::string const& minQuery, std::string const& deleteQuery) { + assert(deleteInterval_); LedgerIndex min = std::numeric_limits::max(); { - auto db = database.checkoutDb(); boost::optional m; - *db << minQuery, soci::into(m); + JLOG(journal_.trace()) + << "Begin: Look up lowest value of: " << minQuery; + { + auto db = database.checkoutDb(); + *db << minQuery, soci::into(m); + } + JLOG(journal_.trace()) << "End: Look up lowest value of: " << minQuery; if (!m) - return false; + return; min = *m; } if (min > lastRotated || health() != Health::ok) - return false; + return; + if (min == lastRotated) + { + // Micro-optimization mainly to clarify logs + JLOG(journal_.trace()) << "Nothing to delete from " << deleteQuery; + return; + } boost::format formattedDeleteQuery(deleteQuery); @@ -587,17 +606,24 @@ SHAMapStoreImp::clearSql( while (min < lastRotated) { min = std::min(lastRotated, min + deleteBatch_); + JLOG(journal_.trace()) << "Begin: Delete up to " << deleteBatch_ + << " rows with LedgerSeq < " << min + << " using query: " << deleteQuery; { auto db = database.checkoutDb(); *db << boost::str(formattedDeleteQuery % min); } + JLOG(journal_.trace()) + << "End: Delete up to " << deleteBatch_ << " rows with LedgerSeq < " + << min << " using query: " << deleteQuery; if (health()) - return true; + return; if (min < lastRotated) - std::this_thread::sleep_for(std::chrono::milliseconds(backOff_)); + std::this_thread::sleep_for(backOff_); + if (health()) + return; } JLOG(journal_.debug()) << "finished: " << deleteQuery; - return true; } void @@ -621,13 +647,14 @@ SHAMapStoreImp::freshenCaches() void SHAMapStoreImp::clearPrior(LedgerIndex lastRotated) { - if (health()) - return; - // Do not allow ledgers to be acquired from the network // that are about to be deleted. minimumOnline_ = lastRotated + 1; + JLOG(journal_.trace()) << "Begin: Clear internal ledgers up to " + << lastRotated; ledgerMaster_->clearPriorLedgers(lastRotated); + JLOG(journal_.trace()) << "End: Clear internal ledgers up to " + << lastRotated; if (health()) return; @@ -666,16 +693,32 @@ SHAMapStoreImp::health() } if (!netOPs_) return Health::ok; + assert(deleteInterval_); - constexpr static std::chrono::seconds age_threshold(60); - auto age = ledgerMaster_->getValidatedLedgerAge(); - OperatingMode mode = netOPs_->getOperatingMode(); - if (mode != OperatingMode::FULL || age > age_threshold) + if (healthy_) { - JLOG(journal_.warn()) << "Not deleting. state: " - << app_.getOPs().strOperatingMode(mode, false) - << ". age " << age.count() << 's'; - healthy_ = false; + auto age = ledgerMaster_->getValidatedLedgerAge(); + OperatingMode mode = netOPs_->getOperatingMode(); + if (recoveryWaitTime_ && mode == OperatingMode::SYNCING && + age < ageThreshold_) + { + JLOG(journal_.warn()) + << "Waiting " << recoveryWaitTime_->count() + << "s for node to get back into sync with network. state: " + << app_.getOPs().strOperatingMode(mode, false) << ". age " + << age.count() << 's'; + std::this_thread::sleep_for(*recoveryWaitTime_); + + age = ledgerMaster_->getValidatedLedgerAge(); + mode = netOPs_->getOperatingMode(); + } + if (mode != OperatingMode::FULL || age > ageThreshold_) + { + JLOG(journal_.warn()) << "Not deleting. state: " + << app_.getOPs().strOperatingMode(mode, false) + << ". age " << age.count() << 's'; + healthy_ = false; + } } if (healthy_) diff --git a/src/ripple/app/misc/SHAMapStoreImp.h b/src/ripple/app/misc/SHAMapStoreImp.h index 2fabf1a6996..6145cb48dfd 100644 --- a/src/ripple/app/misc/SHAMapStoreImp.h +++ b/src/ripple/app/misc/SHAMapStoreImp.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -106,8 +107,14 @@ class SHAMapStoreImp : public SHAMapStore std::uint32_t deleteInterval_ = 0; bool advisoryDelete_ = false; std::uint32_t deleteBatch_ = 100; - std::uint32_t backOff_ = 100; - std::int32_t ageThreshold_ = 60; + std::chrono::milliseconds backOff_{100}; + std::chrono::seconds ageThreshold_{60}; + /// If set, and the node is out of sync during an + /// online_delete health check, sleep the thread + /// for this time and check again so the node can + /// recover. + /// See also: "recovery_wait_seconds" in rippled-example.cfg + boost::optional recoveryWaitTime_; // these do not exist upon SHAMapStore creation, but do exist // as of onPrepare() or before @@ -212,13 +219,11 @@ class SHAMapStoreImp : public SHAMapStore return false; } - /** delete from sqlite table in batches to not lock the db excessively - * pause briefly to extend access time to other users - * call with mutex object unlocked - * @return true if any deletable rows were found (though not - * necessarily deleted. + /** delete from sqlite table in batches to not lock the db excessively. + * Pause briefly to extend access time to other users. + * Call with mutex object unlocked. */ - bool + void clearSql( DatabaseCon& database, LedgerIndex lastRotated, @@ -236,6 +241,9 @@ class SHAMapStoreImp : public SHAMapStore // Assume that, once unhealthy, a necessary step has been // aborted, so the online-delete process needs to restart // at next ledger. + // If recoveryWaitTime_ is set, this may sleep to give rippled + // time to recover, so never call it from any thread other than + // the main "run()". Health health(); // diff --git a/src/ripple/core/DatabaseCon.h b/src/ripple/core/DatabaseCon.h index d79ecef2071..5cdabb08f08 100644 --- a/src/ripple/core/DatabaseCon.h +++ b/src/ripple/core/DatabaseCon.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -89,6 +90,19 @@ class DatabaseCon Config::StartUpType startUp = Config::NORMAL; bool standAlone = false; boost::filesystem::path dataDir; + // Indicates whether or not to return the `globalPragma` + // from commonPragma() + bool useGlobalPragma = false; + + std::vector const* + commonPragma() const + { + assert(!useGlobalPragma || globalPragma); + return useGlobalPragma && globalPragma ? globalPragma.get() + : nullptr; + } + + static std::unique_ptr const> globalPragma; }; template @@ -97,16 +111,17 @@ class DatabaseCon std::string const& DBName, std::array const& pragma, std::array const& initSQL) - { // Use temporary files or regular DB files? - auto const useTempFiles = setup.standAlone && - setup.startUp != Config::LOAD && - setup.startUp != Config::LOAD_FILE && - setup.startUp != Config::REPLAY; - boost::filesystem::path pPath = - useTempFiles ? "" : (setup.dataDir / DBName); - - init(pPath, pragma, initSQL); + : DatabaseCon( + setup.standAlone && setup.startUp != Config::LOAD && + setup.startUp != Config::LOAD_FILE && + setup.startUp != Config::REPLAY + ? "" + : (setup.dataDir / DBName), + setup.commonPragma(), + pragma, + initSQL) + { } template @@ -115,8 +130,8 @@ class DatabaseCon std::string const& DBName, std::array const& pragma, std::array const& initSQL) + : DatabaseCon(dataDir / DBName, nullptr, pragma, initSQL) { - init((dataDir / DBName), pragma, initSQL); } soci::session& @@ -136,14 +151,22 @@ class DatabaseCon private: template - void - init( + DatabaseCon( boost::filesystem::path const& pPath, + std::vector const* commonPragma, std::array const& pragma, std::array const& initSQL) { open(session_, "sqlite", pPath.string()); + if (commonPragma) + { + for (auto const& p : *commonPragma) + { + soci::statement st = session_.prepare << p; + st.execute(true); + } + } for (auto const& p : pragma) { soci::statement st = session_.prepare << p; @@ -163,7 +186,9 @@ class DatabaseCon }; DatabaseCon::Setup -setup_DatabaseCon(Config const& c); +setup_DatabaseCon( + Config const& c, + boost::optional j = boost::none); } // namespace ripple diff --git a/src/ripple/core/impl/Config.cpp b/src/ripple/core/impl/Config.cpp index f12ba7dbcee..a7524574fc8 100644 --- a/src/ripple/core/impl/Config.cpp +++ b/src/ripple/core/impl/Config.cpp @@ -442,7 +442,8 @@ Config::loadFromString(std::string const& fileContents) if (getSingleSection(secConfig, SECTION_LEDGER_HISTORY, strTemp, j_)) { if (boost::iequals(strTemp, "full")) - LEDGER_HISTORY = 1000000000u; + LEDGER_HISTORY = + std::numeric_limits::max(); else if (boost::iequals(strTemp, "none")) LEDGER_HISTORY = 0; else @@ -454,7 +455,7 @@ Config::loadFromString(std::string const& fileContents) if (boost::iequals(strTemp, "none")) FETCH_DEPTH = 0; else if (boost::iequals(strTemp, "full")) - FETCH_DEPTH = 1000000000u; + FETCH_DEPTH = std::numeric_limits::max(); else FETCH_DEPTH = beast::lexicalCastThrow(strTemp); diff --git a/src/ripple/core/impl/DatabaseCon.cpp b/src/ripple/core/impl/DatabaseCon.cpp index 3a4489b2f94..89c4ee1f291 100644 --- a/src/ripple/core/impl/DatabaseCon.cpp +++ b/src/ripple/core/impl/DatabaseCon.cpp @@ -21,12 +21,14 @@ #include #include #include +#include +#include #include namespace ripple { DatabaseCon::Setup -setup_DatabaseCon(Config const& c) +setup_DatabaseCon(Config const& c, boost::optional j) { DatabaseCon::Setup setup; @@ -38,9 +40,134 @@ setup_DatabaseCon(Config const& c) Throw("database_path must be set."); } + if (!setup.globalPragma) + { + setup.globalPragma = [&c, &j]() { + auto const& sqlite = c.section("sqlite"); + auto result = std::make_unique>(); + result->reserve(3); + + // defaults + std::string safety_level; + std::string journal_mode = "wal"; + std::string synchronous = "normal"; + std::string temp_store = "file"; + bool showRiskWarning = false; + + if (set(safety_level, "safety_level", sqlite)) + { + if (boost::iequals(safety_level, "low")) + { + // low safety defaults + journal_mode = "memory"; + synchronous = "off"; + temp_store = "memory"; + showRiskWarning = true; + } + else if (!boost::iequals(safety_level, "high")) + { + Throw( + "Invalid safety_level value: " + safety_level); + } + } + + { + // #journal_mode Valid values : delete, truncate, persist, + // memory, wal, off + if (set(journal_mode, "journal_mode", sqlite) && + !safety_level.empty()) + { + Throw( + "Configuration file may not define both " + "\"safety_level\" and \"journal_mode\""); + } + bool higherRisk = boost::iequals(journal_mode, "memory") || + boost::iequals(journal_mode, "off"); + showRiskWarning = showRiskWarning || higherRisk; + if (higherRisk || boost::iequals(journal_mode, "delete") || + boost::iequals(journal_mode, "truncate") || + boost::iequals(journal_mode, "persist") || + boost::iequals(journal_mode, "wal")) + { + result->emplace_back(boost::str( + boost::format(CommonDBPragmaJournal) % journal_mode)); + } + else + { + Throw( + "Invalid journal_mode value: " + journal_mode); + } + } + + { + //#synchronous Valid values : off, normal, full, extra + if (set(synchronous, "synchronous", sqlite) && + !safety_level.empty()) + { + Throw( + "Configuration file may not define both " + "\"safety_level\" and \"synchronous\""); + } + bool higherRisk = boost::iequals(synchronous, "off"); + showRiskWarning = showRiskWarning || higherRisk; + if (higherRisk || boost::iequals(synchronous, "normal") || + boost::iequals(synchronous, "full") || + boost::iequals(synchronous, "extra")) + { + result->emplace_back(boost::str( + boost::format(CommonDBPragmaSync) % synchronous)); + } + else + { + Throw( + "Invalid synchronous value: " + synchronous); + } + } + + { + // #temp_store Valid values : default, file, memory + if (set(temp_store, "temp_store", sqlite) && + !safety_level.empty()) + { + Throw( + "Configuration file may not define both " + "\"safety_level\" and \"temp_store\""); + } + bool higherRisk = boost::iequals(temp_store, "memory"); + showRiskWarning = showRiskWarning || higherRisk; + if (higherRisk || boost::iequals(temp_store, "default") || + boost::iequals(temp_store, "file")) + { + result->emplace_back(boost::str( + boost::format(CommonDBPragmaTemp) % temp_store)); + } + else + { + Throw( + "Invalid temp_store value: " + temp_store); + } + } + + if (showRiskWarning && j && c.LEDGER_HISTORY > SQLITE_TUNING_CUTOFF) + { + JLOG(j->warn()) + << "reducing the data integrity guarantees from the " + "default [sqlite] behavior is not recommended for " + "nodes storing large amounts of history, because of the " + "difficulty inherent in rebuilding corrupted data."; + } + assert(result->size() == 3); + return result; + }(); + } + setup.useGlobalPragma = true; + return setup; } +std::unique_ptr const> + DatabaseCon::Setup::globalPragma; + void DatabaseCon::setupCheckpointing(JobQueue* q, Logs& l) { diff --git a/src/ripple/net/impl/DatabaseBody.ipp b/src/ripple/net/impl/DatabaseBody.ipp index d6bae7b47f7..5a1bd7e6185 100644 --- a/src/ripple/net/impl/DatabaseBody.ipp +++ b/src/ripple/net/impl/DatabaseBody.ipp @@ -50,7 +50,9 @@ DatabaseBody::value_type::open( auto setup = setup_DatabaseCon(config); setup.dataDir = path.parent_path(); + setup.useGlobalPragma = false; + // Downloader ignores the "CommonPragma" conn_ = std::make_unique( setup, "Download", DownloaderDBPragma, DatabaseBodyDBInit); diff --git a/src/ripple/nodestore/impl/Shard.cpp b/src/ripple/nodestore/impl/Shard.cpp index 1701206fe4d..f8799ff3d27 100644 --- a/src/ripple/nodestore/impl/Shard.cpp +++ b/src/ripple/nodestore/impl/Shard.cpp @@ -124,6 +124,7 @@ Shard::open(Scheduler& scheduler, nudb::context& ctx) setup.startUp = config.START_UP; setup.standAlone = config.standalone(); setup.dataDir = dir_; + setup.useGlobalPragma = true; acquireInfo_->SQLiteDB = std::make_unique( setup, @@ -668,10 +669,14 @@ bool Shard::initSQLite(std::lock_guard const&) { Config const& config{app_.config()}; - DatabaseCon::Setup setup; - setup.startUp = config.START_UP; - setup.standAlone = config.standalone(); - setup.dataDir = dir_; + DatabaseCon::Setup const setup = [&]() { + DatabaseCon::Setup result; + result.startUp = config.START_UP; + result.standAlone = config.standalone(); + result.dataDir = dir_; + result.useGlobalPragma = !backendComplete_; + return result; + }(); try { diff --git a/src/test/app/LedgerHistory_test.cpp b/src/test/app/LedgerHistory_test.cpp index ac2dcda61b2..cbc9c95b325 100644 --- a/src/test/app/LedgerHistory_test.cpp +++ b/src/test/app/LedgerHistory_test.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace ripple { namespace test { @@ -34,56 +35,6 @@ namespace test { class LedgerHistory_test : public beast::unit_test::suite { public: - /** Log manager that searches for a specific message substring - */ - class CheckMessageLogs : public Logs - { - std::string msg_; - bool& found_; - - class CheckMessageSink : public beast::Journal::Sink - { - CheckMessageLogs& owner_; - - public: - CheckMessageSink( - beast::severities::Severity threshold, - CheckMessageLogs& owner) - : beast::Journal::Sink(threshold, false), owner_(owner) - { - } - - void - write(beast::severities::Severity level, std::string const& text) - override - { - if (text.find(owner_.msg_) != std::string::npos) - owner_.found_ = true; - } - }; - - public: - /** Constructor - - @param msg The message string to search for - @param found The variable to set to true if the message is found - */ - CheckMessageLogs(std::string msg, bool& found) - : Logs{beast::severities::kDebug} - , msg_{std::move(msg)} - , found_{found} - { - } - - std::unique_ptr - makeSink( - std::string const& partition, - beast::severities::Severity threshold) override - { - return std::make_unique(threshold, *this); - } - }; - /** Generate a new ledger by hand, applying a specific close time offset and optionally inserting a transaction. @@ -149,7 +100,7 @@ class LedgerHistory_test : public beast::unit_test::suite Env env{ *this, envconfig(), - std::make_unique("MISMATCH ", found)}; + std::make_unique("MISMATCH ", &found)}; LedgerHistory lh{beast::insight::NullCollector::New(), env.app()}; auto const genesis = makeLedger({}, env, lh, 0s); uint256 const dummyTxHash{1}; @@ -166,7 +117,7 @@ class LedgerHistory_test : public beast::unit_test::suite *this, envconfig(), std::make_unique( - "MISMATCH on close time", found)}; + "MISMATCH on close time", &found)}; LedgerHistory lh{beast::insight::NullCollector::New(), env.app()}; auto const genesis = makeLedger({}, env, lh, 0s); auto const ledgerA = makeLedger(genesis, env, lh, 4s); @@ -186,7 +137,7 @@ class LedgerHistory_test : public beast::unit_test::suite *this, envconfig(), std::make_unique( - "MISMATCH on prior ledger", found)}; + "MISMATCH on prior ledger", &found)}; LedgerHistory lh{beast::insight::NullCollector::New(), env.app()}; auto const genesis = makeLedger({}, env, lh, 0s); auto const ledgerA = makeLedger(genesis, env, lh, 4s); @@ -212,7 +163,7 @@ class LedgerHistory_test : public beast::unit_test::suite Env env{ *this, envconfig(), - std::make_unique(msg, found)}; + std::make_unique(msg, &found)}; LedgerHistory lh{beast::insight::NullCollector::New(), env.app()}; Account alice{"A1"}; diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index 063b4383281..18460ce3689 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -256,6 +256,7 @@ class Manifest_test : public beast::unit_test::suite { DatabaseCon::Setup setup; setup.dataDir = getDatabasePath(); + BEAST_EXPECT(!setup.useGlobalPragma); DatabaseCon dbCon( setup, dbName.data(), diff --git a/src/test/jtx/CaptureLogs.h b/src/test/jtx/CaptureLogs.h new file mode 100644 index 00000000000..30a562e99d0 --- /dev/null +++ b/src/test/jtx/CaptureLogs.h @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +/* + 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 { +namespace test { + +/** + * @brief Log manager for CaptureSinks. This class holds the stream + * instance that is written to by the sinks. Upon destruction, all + * contents of the stream are assigned to the string specified in the + * ctor + */ +class CaptureLogs : public Logs +{ + std::stringstream strm_; + std::string* pResult_; + + /** + * @brief sink for writing all log messages to a stringstream + */ + class CaptureSink : public beast::Journal::Sink + { + std::stringstream& strm_; + + public: + CaptureSink( + beast::severities::Severity threshold, + std::stringstream& strm) + : beast::Journal::Sink(threshold, false), strm_(strm) + { + } + + void + write(beast::severities::Severity level, std::string const& text) + override + { + strm_ << text; + } + }; + +public: + explicit CaptureLogs(std::string* pResult) + : Logs(beast::severities::kInfo), pResult_(pResult) + { + } + + ~CaptureLogs() override + { + *pResult_ = strm_.str(); + } + + std::unique_ptr + makeSink( + std::string const& partition, + beast::severities::Severity threshold) override + { + return std::make_unique(threshold, strm_); + } +}; + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/CheckMessageLogs.h b/src/test/jtx/CheckMessageLogs.h new file mode 100644 index 00000000000..66f5f7e106c --- /dev/null +++ b/src/test/jtx/CheckMessageLogs.h @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +/* + 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 { +namespace test { + +/** Log manager that searches for a specific message substring + */ +class CheckMessageLogs : public Logs +{ + std::string msg_; + bool* pFound_; + + class CheckMessageSink : public beast::Journal::Sink + { + CheckMessageLogs& owner_; + + public: + CheckMessageSink( + beast::severities::Severity threshold, + CheckMessageLogs& owner) + : beast::Journal::Sink(threshold, false), owner_(owner) + { + } + + void + write(beast::severities::Severity level, std::string const& text) + override + { + if (text.find(owner_.msg_) != std::string::npos) + *owner_.pFound_ = true; + } + }; + +public: + /** Constructor + + @param msg The message string to search for + @param pFound Pointer to the variable to set to true if the message is + found + */ + CheckMessageLogs(std::string msg, bool* pFound) + : Logs{beast::severities::kDebug}, msg_{std::move(msg)}, pFound_{pFound} + { + } + + std::unique_ptr + makeSink( + std::string const& partition, + beast::severities::Severity threshold) override + { + return std::make_unique(threshold, *this); + } +}; + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index f06cfbf7a9c..f2934bb5002 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -27,6 +27,7 @@ #include #include #include // +#include #include #include #include @@ -131,7 +132,8 @@ class Env AppBundle( beast::unit_test::suite& suite, std::unique_ptr config, - std::unique_ptr logs); + std::unique_ptr logs, + beast::severities::Severity thresh); ~AppBundle(); }; @@ -163,12 +165,10 @@ class Env Env(beast::unit_test::suite& suite_, std::unique_ptr config, FeatureBitset features, - std::unique_ptr logs = nullptr) + std::unique_ptr logs = nullptr, + beast::severities::Severity thresh = beast::severities::kError) : test(suite_) - , bundle_( - suite_, - std::move(config), - logs ? std::move(logs) : std::make_unique(suite_)) + , bundle_(suite_, std::move(config), std::move(logs), thresh) , journal{bundle_.app->journal("Env")} { memoize(Account::master); @@ -211,11 +211,13 @@ class Env */ Env(beast::unit_test::suite& suite_, std::unique_ptr config, - std::unique_ptr logs = nullptr) + std::unique_ptr logs = nullptr, + beast::severities::Severity thresh = beast::severities::kError) : Env(suite_, std::move(config), supported_amendments(), - std::move(logs)) + std::move(logs), + thresh) { } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index a9b7c3430ff..855dfe7bbf0 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -59,12 +59,22 @@ namespace jtx { Env::AppBundle::AppBundle( beast::unit_test::suite& suite, std::unique_ptr config, - std::unique_ptr logs) + std::unique_ptr logs, + beast::severities::Severity thresh) : AppBundle() { using namespace beast::severities; - // Use kFatal threshold to reduce noise from STObject. - setDebugLogSink(std::make_unique("Debug", kFatal, suite)); + if (logs) + { + setDebugLogSink(logs->makeSink("Debug", kFatal)); + } + else + { + logs = std::make_unique(suite); + // Use kFatal threshold to reduce noise from STObject. + setDebugLogSink( + std::make_unique("Debug", kFatal, suite)); + } auto timeKeeper_ = std::make_unique(); timeKeeper = timeKeeper_.get(); // Hack so we don't have to call Config::setup @@ -72,7 +82,7 @@ Env::AppBundle::AppBundle( owned = make_Application( std::move(config), std::move(logs), std::move(timeKeeper_)); app = owned.get(); - app->logs().threshold(kError); + app->logs().threshold(thresh); if (!app->setup()) Throw("Env::AppBundle: setup failed"); timeKeeper->set(app->getLedgerMaster().getClosedLedger()->info().closeTime); diff --git a/src/test/nodestore/Database_test.cpp b/src/test/nodestore/Database_test.cpp index b1a88bea557..826f5ccf5bf 100644 --- a/src/test/nodestore/Database_test.cpp +++ b/src/test/nodestore/Database_test.cpp @@ -18,8 +18,12 @@ //============================================================================== #include +#include #include #include +#include +#include +#include #include #include @@ -35,6 +39,409 @@ class Database_test : public TestBase { } + void + testConfig() + { + testcase("Config"); + + using namespace ripple::test; + using namespace ripple::test::jtx; + + auto const integrityWarning = + "reducing the data integrity guarantees from the " + "default [sqlite] behavior is not recommended for " + "nodes storing large amounts of history, because of the " + "difficulty inherent in rebuilding corrupted data."; + { + // defaults + Env env(*this); + + auto const s = setup_DatabaseCon(env.app().config()); + + if (BEAST_EXPECT(s.globalPragma->size() == 3)) + { + BEAST_EXPECT( + s.globalPragma->at(0) == "PRAGMA journal_mode=wal;"); + BEAST_EXPECT( + s.globalPragma->at(1) == "PRAGMA synchronous=normal;"); + BEAST_EXPECT( + s.globalPragma->at(2) == "PRAGMA temp_store=file;"); + } + } + { + // High safety level + DatabaseCon::Setup::globalPragma.reset(); + + bool found = false; + Env env = [&]() { + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); + } + p->LEDGER_HISTORY = 100'000'000; + + return Env( + *this, + std::move(p), + std::make_unique( + integrityWarning, &found), + beast::severities::kWarning); + }(); + + BEAST_EXPECT(!found); + auto const s = setup_DatabaseCon(env.app().config()); + if (BEAST_EXPECT(s.globalPragma->size() == 3)) + { + BEAST_EXPECT( + s.globalPragma->at(0) == "PRAGMA journal_mode=wal;"); + BEAST_EXPECT( + s.globalPragma->at(1) == "PRAGMA synchronous=normal;"); + BEAST_EXPECT( + s.globalPragma->at(2) == "PRAGMA temp_store=file;"); + } + } + { + // Low safety level + DatabaseCon::Setup::globalPragma.reset(); + + bool found = false; + Env env = [&]() { + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); + } + p->LEDGER_HISTORY = 100'000'000; + + return Env( + *this, + std::move(p), + std::make_unique( + integrityWarning, &found), + beast::severities::kWarning); + }(); + + BEAST_EXPECT(found); + auto const s = setup_DatabaseCon(env.app().config()); + if (BEAST_EXPECT(s.globalPragma->size() == 3)) + { + BEAST_EXPECT( + s.globalPragma->at(0) == "PRAGMA journal_mode=memory;"); + BEAST_EXPECT( + s.globalPragma->at(1) == "PRAGMA synchronous=off;"); + BEAST_EXPECT( + s.globalPragma->at(2) == "PRAGMA temp_store=memory;"); + } + } + { + // Override individual settings + DatabaseCon::Setup::globalPragma.reset(); + + bool found = false; + Env env = [&]() { + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); + } + + return Env( + *this, + std::move(p), + std::make_unique( + integrityWarning, &found), + beast::severities::kWarning); + }(); + + // No warning, even though higher risk settings were used because + // LEDGER_HISTORY is small + BEAST_EXPECT(!found); + auto const s = setup_DatabaseCon(env.app().config()); + if (BEAST_EXPECT(s.globalPragma->size() == 3)) + { + BEAST_EXPECT( + s.globalPragma->at(0) == "PRAGMA journal_mode=off;"); + BEAST_EXPECT( + s.globalPragma->at(1) == "PRAGMA synchronous=extra;"); + BEAST_EXPECT( + s.globalPragma->at(2) == "PRAGMA temp_store=default;"); + } + } + { + // Override individual settings with large history + DatabaseCon::Setup::globalPragma.reset(); + + bool found = false; + Env env = [&]() { + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); + } + p->LEDGER_HISTORY = 50'000'000; + + return Env( + *this, + std::move(p), + std::make_unique( + integrityWarning, &found), + beast::severities::kWarning); + }(); + + // No warning, even though higher risk settings were used because + // LEDGER_HISTORY is small + BEAST_EXPECT(found); + auto const s = setup_DatabaseCon(env.app().config()); + if (BEAST_EXPECT(s.globalPragma->size() == 3)) + { + BEAST_EXPECT( + s.globalPragma->at(0) == "PRAGMA journal_mode=off;"); + BEAST_EXPECT( + s.globalPragma->at(1) == "PRAGMA synchronous=extra;"); + BEAST_EXPECT( + s.globalPragma->at(2) == "PRAGMA temp_store=default;"); + } + } + { + // Error: Mix safety_level and individual settings + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: " + "Configuration file may not define both \"safety_level\" and " + "\"journal_mode\""; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Mix safety_level and one setting (gotta catch 'em all) + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Configuration file may " + "not define both \"safety_level\" and \"journal_mode\""; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); + section.set("journal_mode", "off"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Mix safety_level and one setting (gotta catch 'em all) + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Configuration file may " + "not define both \"safety_level\" and \"synchronous\""; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); + section.set("synchronous", "extra"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Mix safety_level and one setting (gotta catch 'em all) + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Configuration file may " + "not define both \"safety_level\" and \"temp_store\""; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); + section.set("temp_store", "default"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Invalid value + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Invalid safety_level " + "value: slow"; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("safety_level", "slow"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Invalid value + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Invalid journal_mode " + "value: fast"; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("journal_mode", "fast"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Invalid value + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Invalid synchronous " + "value: instant"; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("synchronous", "instant"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + { + // Error: Invalid value + DatabaseCon::Setup::globalPragma.reset(); + auto const expected = + "Failed to initialize SQLite databases: Invalid temp_store " + "value: network"; + bool found = false; + + auto p = test::jtx::envconfig(); + { + auto& section = p->section("sqlite"); + section.set("temp_store", "network"); + } + + try + { + Env env( + *this, + std::move(p), + std::make_unique(expected, &found), + beast::severities::kWarning); + fail(); + } + catch (...) + { + BEAST_EXPECT(found); + } + } + } + + //-------------------------------------------------------------------------- + void testImport( std::string const& destBackendType, @@ -221,6 +628,8 @@ class Database_test : public TestBase { std::int64_t const seedValue = 50; + testConfig(); + testNodeStore("memory", false, seedValue); // Persistent backend tests diff --git a/src/test/server/Server_test.cpp b/src/test/server/Server_test.cpp index ef132d2eb0c..521661c5895 100644 --- a/src/test/server/Server_test.cpp +++ b/src/test/server/Server_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -375,60 +376,6 @@ class Server_test : public beast::unit_test::suite pass(); } - /** - * @brief sink for writing all log messages to a stringstream - */ - class CaptureSink : public beast::Journal::Sink - { - std::stringstream& strm_; - - public: - CaptureSink( - beast::severities::Severity threshold, - std::stringstream& strm) - : beast::Journal::Sink(threshold, false), strm_(strm) - { - } - - void - write(beast::severities::Severity level, std::string const& text) - override - { - strm_ << text; - } - }; - - /** - * @brief Log manager for CaptureSinks. This class holds the stream - * instance that is written to by the sinks. Upon destruction, all - * contents of the stream are assigned to the string specified in the - * ctor - */ - class CaptureLogs : public Logs - { - std::stringstream strm_; - std::string& result_; - - public: - explicit CaptureLogs(std::string& result) - : Logs(beast::severities::kInfo), result_(result) - { - } - - ~CaptureLogs() override - { - result_ = strm_.str(); - } - - std::unique_ptr - makeSink( - std::string const& partition, - beast::severities::Severity threshold) override - { - return std::make_unique(threshold, strm_); - } - }; - void testBadConfig() { @@ -444,7 +391,7 @@ class Server_test : public beast::unit_test::suite (*cfg).deprecatedClearSection("port_rpc"); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Missing 'ip' in [port_rpc]") != std::string::npos); @@ -457,7 +404,7 @@ class Server_test : public beast::unit_test::suite (*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr()); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Missing 'port' in [port_rpc]") != std::string::npos); @@ -471,7 +418,7 @@ class Server_test : public beast::unit_test::suite (*cfg)["port_rpc"].set("port", "0"); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Invalid value '0' for key 'port' in [port_rpc]") != @@ -487,7 +434,7 @@ class Server_test : public beast::unit_test::suite (*cfg)["port_rpc"].set("protocol", ""); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Missing 'protocol' in [port_rpc]") != @@ -522,7 +469,7 @@ class Server_test : public beast::unit_test::suite (*cfg)["port_ws"].set("admin", getEnvLocalhostAddr()); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Required section [server] is missing") != @@ -548,7 +495,7 @@ class Server_test : public beast::unit_test::suite (*cfg)["server"].append("port_ws"); return cfg; }), - std::make_unique(messages)}; + std::make_unique(&messages)}; }); BEAST_EXPECT( messages.find("Missing section: [port_peer]") != std::string::npos);