Improve online_delete configuration and DB tuning:

* Document delete_batch, back_off_milliseconds, age_threshold_seconds.
* Convert those time values to chrono types.
* Fix bug that ignored age_threshold_seconds.
* Add a "recovery buffer" to the config that gives the node a chance to
  recover before aborting online delete.
* Add begin/end log messages around the SQL queries.
* Add a new configuration section: [sqlite] to allow tuning the sqlite
  database operations. Ignored on full/large history servers.
* Update documentation of [node_db] and [sqlite] in the
  rippled-example.cfg file.

Resolves #3321
This commit is contained in:
Edward Hennis
2020-05-11 16:48:34 -04:00
committed by Nik Bougalis
parent 00702f28c2
commit 4702c8b591
21 changed files with 1086 additions and 271 deletions

View File

@@ -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)
#
# 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.
# Format (without spaces):
# One or more lines of case-insensitive key / value pairs:
# <key> '=' <value>
# ...
#
# 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:
#
# 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

View File

@@ -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();

View File

@@ -1019,7 +1019,7 @@ public:
try
{
auto const setup = setup_DatabaseCon(*config_);
auto setup = setup_DatabaseCon(*config_, m_journal);
// transaction database
mTxnDB = std::make_unique<DatabaseCon>(
@@ -1069,6 +1069,7 @@ public:
mLedgerDB->setupCheckpointing(m_jobQueue.get(), logs());
// wallet database
setup.useGlobalPragma = false;
mWalletDB = std::make_unique<DatabaseCon>(
setup,
WalletDBName,
@@ -1360,7 +1361,7 @@ public:
JLOG(m_journal.fatal())
<< "Free SQLite space for transaction db is less than "
"512MB. To fix this, rippled must be executed with the "
"vacuum <sqlitetmpdir> parameter before restarting. "
"\"--vacuum\" parameter before restarting. "
"Note that this activity can take multiple days, "
"depending on database size.";
signalStop();

View File

@@ -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<char const*, 3> LgrDBPragma{
{"PRAGMA synchronous=NORMAL;",
"PRAGMA journal_mode=WAL;",
"PRAGMA journal_size_limit=1582080;"}};
inline constexpr std::array<char const*, 1> LgrDBPragma{
{"PRAGMA journal_size_limit=1582080;"}};
inline constexpr std::array<char const*, 5> LgrDBInit{
{"BEGIN TRANSACTION;",
@@ -61,22 +71,13 @@ inline constexpr std::array<char const*, 5> 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<char const*, 6>
TxDBPragma
inline constexpr std::array TxDBPragma
{
{
#else
std::array<char const*, 5> TxDBPragma {{
#endif
"PRAGMA page_size=4096;", "PRAGMA synchronous=NORMAL;",
"PRAGMA journal_mode=WAL;", "PRAGMA journal_size_limit=1582080;",
"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;"
#endif
}
};
inline constexpr std::array<char const*, 8> TxDBInit{
@@ -115,10 +116,8 @@ inline constexpr std::array<char const*, 8> TxDBInit{
// Temporary database used with an incomplete shard that is being acquired
inline constexpr auto AcquireShardDBName{"acquire.db"};
inline constexpr std::array<char const*, 3> AcquireShardDBPragma{
{"PRAGMA synchronous=NORMAL;",
"PRAGMA journal_mode=WAL;",
"PRAGMA journal_size_limit=1582080;"}};
inline constexpr std::array<char const*, 1> AcquireShardDBPragma{
{"PRAGMA journal_size_limit=1582080;"}};
inline constexpr std::array<char const*, 1> AcquireShardDBInit{
{"CREATE TABLE IF NOT EXISTS Shard ( \
@@ -130,6 +129,7 @@ inline constexpr std::array<char const*, 1> AcquireShardDBInit{
////////////////////////////////////////////////////////////////////////////////
// Pragma for Ledger and Transaction databases with complete shards
// These override the CommonDBPragma values defined above.
inline constexpr std::array<char const*, 2> CompleteShardDBPragma{
{"PRAGMA synchronous=OFF;", "PRAGMA journal_mode=OFF;"}};
@@ -172,6 +172,7 @@ inline constexpr std::array<char const*, 6> WalletDBInit{
static constexpr auto stateDBName{"state.db"};
// These override the CommonDBPragma values defined above.
static constexpr std::array<char const*, 2> DownloaderDBPragma{
{"PRAGMA synchronous=FULL;", "PRAGMA journal_mode=DELETE;"}};

View File

@@ -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<std::string>(),
"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<std::string>();
try
{
uintmax_t const dbSize = file_size(dbPath);
assert(dbSize != static_cast<uintmax_t>(-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

View File

@@ -2757,12 +2757,12 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
if (std::abs(closeOffset.count()) >= 60)
l[jss::close_time_offset] = closeOffset.count();
constexpr std::chrono::seconds HIGH_AGE_THRESHOLD{1000000};
constexpr std::chrono::seconds highAgeThreshold{1000000};
if (m_ledgerMaster.haveValidated())
{
auto const age = m_ledgerMaster.getValidatedLedgerAge();
l[jss::age] =
Json::UInt(age < HIGH_AGE_THRESHOLD ? age.count() : 0);
Json::UInt(age < highAgeThreshold ? age.count() : 0);
}
else
{
@@ -2773,7 +2773,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
using namespace std::chrono_literals;
auto age = closeTime - lCloseTime;
l[jss::age] =
Json::UInt(age < HIGH_AGE_THRESHOLD ? age.count() : 0);
Json::UInt(age < highAgeThreshold ? age.count() : 0);
}
}
}

View File

@@ -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,27 +380,29 @@ 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));
switch (health())
{
case Health::stopping:
stopped();
return;
case Health::unhealthy:
continue;
case Health::ok:
default:;
}
// Only log if we completed without a "health" abort
JLOG(journal_.debug()) << "copied ledger " << validatedSeq
<< " nodecount " << nodeCount;
switch (health())
{
case Health::stopping:
stopped();
return;
case Health::unhealthy:
continue;
case Health::ok:
default:;
}
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<LedgerIndex>::max();
{
auto db = database.checkoutDb();
boost::optional<std::uint64_t> 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,17 +693,33 @@ SHAMapStoreImp::health()
}
if (!netOPs_)
return Health::ok;
assert(deleteInterval_);
constexpr static std::chrono::seconds age_threshold(60);
if (healthy_)
{
auto age = ledgerMaster_->getValidatedLedgerAge();
OperatingMode mode = netOPs_->getOperatingMode();
if (mode != OperatingMode::FULL || age > age_threshold)
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_)
return Health::ok;

View File

@@ -25,6 +25,7 @@
#include <ripple/core/DatabaseCon.h>
#include <ripple/nodestore/DatabaseRotating.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <thread>
@@ -106,8 +107,14 @@ private:
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<std::chrono::seconds> recoveryWaitTime_;
// these do not exist upon SHAMapStore creation, but do exist
// as of onPrepare() or before
@@ -212,13 +219,11 @@ private:
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 @@ private:
// 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();
//

View File

@@ -24,6 +24,7 @@
#include <ripple/core/Config.h>
#include <ripple/core/SociDB.h>
#include <boost/filesystem/path.hpp>
#include <boost/optional.hpp>
#include <mutex>
#include <string>
@@ -89,6 +90,19 @@ public:
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<std::string> const*
commonPragma() const
{
assert(!useGlobalPragma || globalPragma);
return useGlobalPragma && globalPragma ? globalPragma.get()
: nullptr;
}
static std::unique_ptr<std::vector<std::string> const> globalPragma;
};
template <std::size_t N, std::size_t M>
@@ -97,16 +111,17 @@ public:
std::string const& DBName,
std::array<char const*, N> const& pragma,
std::array<char const*, M> const& initSQL)
{
// Use temporary files or regular DB files?
auto const useTempFiles = setup.standAlone &&
setup.startUp != Config::LOAD &&
: DatabaseCon(
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);
setup.startUp != Config::REPLAY
? ""
: (setup.dataDir / DBName),
setup.commonPragma(),
pragma,
initSQL)
{
}
template <std::size_t N, std::size_t M>
@@ -115,8 +130,8 @@ public:
std::string const& DBName,
std::array<char const*, N> const& pragma,
std::array<char const*, M> const& initSQL)
: DatabaseCon(dataDir / DBName, nullptr, pragma, initSQL)
{
init((dataDir / DBName), pragma, initSQL);
}
soci::session&
@@ -136,14 +151,22 @@ public:
private:
template <std::size_t N, std::size_t M>
void
init(
DatabaseCon(
boost::filesystem::path const& pPath,
std::vector<std::string> const* commonPragma,
std::array<char const*, N> const& pragma,
std::array<char const*, M> 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 @@ private:
};
DatabaseCon::Setup
setup_DatabaseCon(Config const& c);
setup_DatabaseCon(
Config const& c,
boost::optional<beast::Journal> j = boost::none);
} // namespace ripple

View File

@@ -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<decltype(LEDGER_HISTORY)>::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<decltype(FETCH_DEPTH)>::max();
else
FETCH_DEPTH = beast::lexicalCastThrow<std::uint32_t>(strTemp);

View File

@@ -21,12 +21,14 @@
#include <ripple/basics/contract.h>
#include <ripple/core/DatabaseCon.h>
#include <ripple/core/SociDB.h>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include <memory>
namespace ripple {
DatabaseCon::Setup
setup_DatabaseCon(Config const& c)
setup_DatabaseCon(Config const& c, boost::optional<beast::Journal> j)
{
DatabaseCon::Setup setup;
@@ -38,9 +40,134 @@ setup_DatabaseCon(Config const& c)
Throw<std::runtime_error>("database_path must be set.");
}
if (!setup.globalPragma)
{
setup.globalPragma = [&c, &j]() {
auto const& sqlite = c.section("sqlite");
auto result = std::make_unique<std::vector<std::string>>();
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<std::runtime_error>(
"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<std::runtime_error>(
"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<std::runtime_error>(
"Invalid journal_mode value: " + journal_mode);
}
}
{
//#synchronous Valid values : off, normal, full, extra
if (set(synchronous, "synchronous", sqlite) &&
!safety_level.empty())
{
Throw<std::runtime_error>(
"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<std::runtime_error>(
"Invalid synchronous value: " + synchronous);
}
}
{
// #temp_store Valid values : default, file, memory
if (set(temp_store, "temp_store", sqlite) &&
!safety_level.empty())
{
Throw<std::runtime_error>(
"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<std::runtime_error>(
"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<std::vector<std::string> const>
DatabaseCon::Setup::globalPragma;
void
DatabaseCon::setupCheckpointing(JobQueue* q, Logs& l)
{

View File

@@ -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<DatabaseCon>(
setup, "Download", DownloaderDBPragma, DatabaseBodyDBInit);

View File

@@ -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<DatabaseCon>(
setup,
@@ -668,10 +669,14 @@ bool
Shard::initSQLite(std::lock_guard<std::recursive_mutex> 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
{

View File

@@ -27,6 +27,7 @@
#include <memory>
#include <sstream>
#include <test/jtx.h>
#include <test/jtx/CheckMessageLogs.h>
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<beast::Journal::Sink>
makeSink(
std::string const& partition,
beast::severities::Severity threshold) override
{
return std::make_unique<CheckMessageSink>(threshold, *this);
}
};
/** Generate a new ledger by hand, applying a specific close time offset
and optionally inserting a transaction.
@@ -149,7 +100,7 @@ public:
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>("MISMATCH ", found)};
std::make_unique<CheckMessageLogs>("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 @@ public:
*this,
envconfig(),
std::make_unique<CheckMessageLogs>(
"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 @@ public:
*this,
envconfig(),
std::make_unique<CheckMessageLogs>(
"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 @@ public:
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>(msg, found)};
std::make_unique<CheckMessageLogs>(msg, &found)};
LedgerHistory lh{beast::insight::NullCollector::New(), env.app()};
Account alice{"A1"};

View File

@@ -256,6 +256,7 @@ public:
{
DatabaseCon::Setup setup;
setup.dataDir = getDatabasePath();
BEAST_EXPECT(!setup.useGlobalPragma);
DatabaseCon dbCon(
setup,
dbName.data(),

View File

@@ -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 <ripple/basics/Log.h>
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<beast::Journal::Sink>
makeSink(
std::string const& partition,
beast::severities::Severity threshold) override
{
return std::make_unique<CaptureSink>(threshold, strm_);
}
};
} // namespace test
} // namespace ripple

View File

@@ -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 <ripple/basics/Log.h>
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<beast::Journal::Sink>
makeSink(
std::string const& partition,
beast::severities::Severity threshold) override
{
return std::make_unique<CheckMessageSink>(threshold, *this);
}
};
} // namespace test
} // namespace ripple

View File

@@ -27,6 +27,7 @@
#include <ripple/basics/Log.h>
#include <ripple/basics/chrono.h>
#include <ripple/beast/cxx17/type_traits.h> // <type_traits>
#include <ripple/beast/utility/Journal.h>
#include <ripple/core/Config.h>
#include <ripple/json/json_value.h>
#include <ripple/json/to_string.h>
@@ -131,7 +132,8 @@ private:
AppBundle(
beast::unit_test::suite& suite,
std::unique_ptr<Config> config,
std::unique_ptr<Logs> logs);
std::unique_ptr<Logs> logs,
beast::severities::Severity thresh);
~AppBundle();
};
@@ -163,12 +165,10 @@ public:
Env(beast::unit_test::suite& suite_,
std::unique_ptr<Config> config,
FeatureBitset features,
std::unique_ptr<Logs> logs = nullptr)
std::unique_ptr<Logs> logs = nullptr,
beast::severities::Severity thresh = beast::severities::kError)
: test(suite_)
, bundle_(
suite_,
std::move(config),
logs ? std::move(logs) : std::make_unique<SuiteLogs>(suite_))
, bundle_(suite_, std::move(config), std::move(logs), thresh)
, journal{bundle_.app->journal("Env")}
{
memoize(Account::master);
@@ -211,11 +211,13 @@ public:
*/
Env(beast::unit_test::suite& suite_,
std::unique_ptr<Config> config,
std::unique_ptr<Logs> logs = nullptr)
std::unique_ptr<Logs> logs = nullptr,
beast::severities::Severity thresh = beast::severities::kError)
: Env(suite_,
std::move(config),
supported_amendments(),
std::move(logs))
std::move(logs),
thresh)
{
}

View File

@@ -59,12 +59,22 @@ namespace jtx {
Env::AppBundle::AppBundle(
beast::unit_test::suite& suite,
std::unique_ptr<Config> config,
std::unique_ptr<Logs> logs)
std::unique_ptr<Logs> logs,
beast::severities::Severity thresh)
: AppBundle()
{
using namespace beast::severities;
if (logs)
{
setDebugLogSink(logs->makeSink("Debug", kFatal));
}
else
{
logs = std::make_unique<SuiteLogs>(suite);
// Use kFatal threshold to reduce noise from STObject.
setDebugLogSink(std::make_unique<SuiteJournalSink>("Debug", kFatal, suite));
setDebugLogSink(
std::make_unique<SuiteJournalSink>("Debug", kFatal, suite));
}
auto timeKeeper_ = std::make_unique<ManualTimeKeeper>();
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<std::runtime_error>("Env::AppBundle: setup failed");
timeKeeper->set(app->getLedgerMaster().getClosedLedger()->info().closeTime);

View File

@@ -18,8 +18,12 @@
//==============================================================================
#include <ripple/beast/utility/temp_dir.h>
#include <ripple/core/DatabaseCon.h>
#include <ripple/nodestore/DummyScheduler.h>
#include <ripple/nodestore/Manager.h>
#include <test/jtx.h>
#include <test/jtx/CheckMessageLogs.h>
#include <test/jtx/envconfig.h>
#include <test/nodestore/TestBase.h>
#include <test/unit_test/SuiteJournal.h>
@@ -35,6 +39,409 @@ public:
{
}
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<CheckMessageLogs>(
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<CheckMessageLogs>(
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<CheckMessageLogs>(
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<CheckMessageLogs>(
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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(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<CheckMessageLogs>(expected, &found),
beast::severities::kWarning);
fail();
}
catch (...)
{
BEAST_EXPECT(found);
}
}
}
//--------------------------------------------------------------------------
void
testImport(
std::string const& destBackendType,
@@ -221,6 +628,8 @@ public:
{
std::int64_t const seedValue = 50;
testConfig();
testNodeStore("memory", false, seedValue);
// Persistent backend tests

View File

@@ -31,6 +31,7 @@
#include <chrono>
#include <stdexcept>
#include <test/jtx.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/envconfig.h>
#include <test/unit_test/SuiteJournal.h>
#include <thread>
@@ -375,60 +376,6 @@ public:
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<beast::Journal::Sink>
makeSink(
std::string const& partition,
beast::severities::Severity threshold) override
{
return std::make_unique<CaptureSink>(threshold, strm_);
}
};
void
testBadConfig()
{
@@ -444,7 +391,7 @@ public:
(*cfg).deprecatedClearSection("port_rpc");
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Missing 'ip' in [port_rpc]") != std::string::npos);
@@ -457,7 +404,7 @@ public:
(*cfg)["port_rpc"].set("ip", getEnvLocalhostAddr());
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Missing 'port' in [port_rpc]") != std::string::npos);
@@ -471,7 +418,7 @@ public:
(*cfg)["port_rpc"].set("port", "0");
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Invalid value '0' for key 'port' in [port_rpc]") !=
@@ -487,7 +434,7 @@ public:
(*cfg)["port_rpc"].set("protocol", "");
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Missing 'protocol' in [port_rpc]") !=
@@ -522,7 +469,7 @@ public:
(*cfg)["port_ws"].set("admin", getEnvLocalhostAddr());
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Required section [server] is missing") !=
@@ -548,7 +495,7 @@ public:
(*cfg)["server"].append("port_ws");
return cfg;
}),
std::make_unique<CaptureLogs>(messages)};
std::make_unique<CaptureLogs>(&messages)};
});
BEAST_EXPECT(
messages.find("Missing section: [port_peer]") != std::string::npos);