mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 00:36:48 +00:00
7.7 KiB
7.7 KiB
SQL Database
SQLite via SOCI for ledger/transaction history. Only SQLite is supported; the backend name is validated and any non-sqlite value throws at config parse time.
Key Invariants
- Two main databases:
lgrdb_(ledger) andtxdb_(transactions, optional viauseTxTablesconfig) - Transaction tables are optional; disabling them means no transaction history or account_tx queries
- WAL checkpointing offloads to
JobQueue(jtWAL); at most one checkpoint job in flight perDatabaseCon(guarded byrunning_mutex) - Database init failure is fatal (throws exception, prevents construction)
- Free disk space < 512MB triggers fatal error on write operations
- File extension inconsistency:
validatorsandpeerfinderuse.sqlite; all other DBs use.db. This is historical and enforced indetail::getSociInit
Schema
Ledgerstable: seq, hash, parent hash, total coins, close time, etc. Indexed byLedgerSeqTransactionstable: TransID, TransType, FromAcct, FromSeq, LedgerSeq, Status, RawTxn, TxnMeta. Indexed byLedgerSeqAccountTransactionstable: TransID, Account, LedgerSeq, TxnSeq. Triple-indexed for account_tx queries- Secondary DBs: Wallet (node identity, manifests), PeerFinder (bootstrap cache), State (deletion tracking)
Common Bug Patterns
- No schema migration system;
CREATE TABLE IF NOT EXISTSmeans old schemas silently persist with missing columns - PeerFinder DB is the exception — it has schema versioning via
SchemaVersiontable safety_levelconfig affects journal_mode and synchronous; "low" can lose data on crashpage_sizemust be power of 2 between 512-65536; invalid values cause init failure- Online deletion coordinates between NodeStore rotation and SQL table pruning; race conditions here lose history
- Empty database name passed to
detail::getSociSqliteInitthrows — silent fallback paths are not provided - A
WALCheckpointerregistered withsqlite3_wal_hookoutlives itsDatabaseConif a checkpoint job is in flight; teardown must wait for the job to drain (see Lifecycle below)
Configuration
| Option | Section | Values | Default |
|---|---|---|---|
backend |
[sqdb] / [relational_db] |
sqlite only |
sqlite |
page_size |
[sqlite] |
512-65536, power of 2 | 4096 |
safety_level |
[sqlite] |
high, medium, low | high |
journal_size_limit |
[sqlite] |
integer >= 0 | 1582080 |
WAL Checkpointer Lifecycle
The checkpointer subsystem is the trickiest part of this module. SQLite's WAL hook is a C callback registered on the native sqlite3* connection, but the work runs on a JobQueue thread that may still be executing when the owning DatabaseCon is destroyed.
ID-based hook indirection
WALCheckpointer(inSociDB.cpp) is registered withsqlite3_wal_hookusing astd::uintptr_t id_cast tovoid*, not a rawthispointer.- The C hook calls
checkpointerFromId()which looks up the ID in a process-wideCheckpointersCollection(inDatabaseCon.cpp). If the lookup returns null, the hook deregisters itself viasqlite3_wal_hook(conn, nullptr, nullptr). - This protects against the hook firing on a writer thread between the
DatabaseConbeing torn down and the hook being unwired.
Session ownership split
DatabaseConholdsstd::shared_ptr<soci::session>.WALCheckpointerholds onlystd::weak_ptr<soci::session>. Intentional: if the checkpointer held ashared_ptr, an in-flight job would keep the WAL lock alive and a freshly-opened replacementDatabaseConwould fail to acquire it.WALCheckpointer::checkpoint()callssession_.lock()and bails silently if expired.
Destructor wait
DatabaseCon::~DatabaseCon sequence (order matters):
checkpointers.erase(checkpointer_->id())— future hook invocations now no-op.- Take a
weak_ptrto the checkpointer, thencheckpointer_.reset(). - Busy-poll
wk.use_count() != 0with 100 ms sleeps until all in-flight job lambdas release theirshared_ptr<Checkpointer>.
The 100 ms poll is deliberate (rare event, simpler than a condvar). Without this wait, reopening the same SQLite file immediately after destruction can fail because the old checkpoint job still holds the WAL lock.
Checkpoint job behavior
- Triggered by
sqlite3_wal_hookafter every WAL write; module-levelcheckpointPageCount = 1000mirrors SQLite's auto-checkpoint threshold. schedule()uses arunning_bool under a mutex to ensure single in-flight job; ifJobQueuerejects the job,running_is reset.- The enqueued lambda captures
std::weak_ptr<Checkpointer>so a destroyedDatabaseConcauses the job to exit without touching the session. checkpoint()callssqlite3_wal_checkpoint_v2withSQLITE_CHECKPOINT_PASSIVE.SQLITE_LOCKEDis logged at trace (expected under reader contention); other errors are warnings.- Net effect: routes checkpoint work off the writer thread onto
jtWAL. SQLite would otherwise do this synchronously on whichever thread crossed the page threshold.
setupCheckpointing
- Separated from
DatabaseConconstructors so checkpointing is opt-in. - Constructors taking a
CheckpointerSetupopen the DB first, then callsetupCheckpointing(JobQueue*, ServiceRegistry&). - Null
JobQueue*throwsstd::logic_error(programming error, not runtime). - The checkpointer must be inserted into
CheckpointersCollectionbefore returning from setup, because the WAL hook is armed inside theWALCheckpointerconstructor and writes can fire it immediately.
SOCI Adapter Notes
getConnection(session&)(SociDB.cpp) recovers the rawsqlite3*viadynamic_cast<soci::sqlite3_session_backend*>. This is the only intentional break in the SOCI abstraction; needed for WAL hooks andsqlite3_db_status.getKBUsedAll()→sqlite3_memory_used()(process-global).getKBUsedDB()→SQLITE_DBSTATUS_CACHE_USED(per-connection).- Four
convert()overloads bridgesoci::blobandstd::vector<uint8_t>/std::string. Empty blobs requireblob.trim(0)rather thanblob.write(nullptr, 0). SociDB.cppopens with#pragma clang diagnostic ignored "-Wdeprecated"because SOCI headers use deprecated constructs; scoped to this TU only.DBConfigis two-phase: parse params, open later.detail::getSociInitanddetail::getSociSqliteInitresolve backend + path; the.sqlitevs.dbextension fork lives ingetSociInit.
Key Patterns
Schema Evolution Caveat
// WARNING: no migration system — old databases keep old schemas
// CREATE TABLE IF NOT EXISTS silently skips if table exists with old columns
// New columns on existing tables require manual ALTER TABLE or
// documentation that the column is optional and may be absent
Disk Space Guard
// REQUIRED on write paths: < 512MB triggers fatal to prevent corruption
if (freeDiskSpace < minDiskFree)
Throw<std::runtime_error>("Not enough disk space for database write");
WAL Hook Cookie
// Always pass an integer ID, never `this`. The DatabaseCon may be
// destroyed while a hook invocation is mid-flight on a writer thread.
sqlite3_wal_hook(conn, &walHookCallback,
reinterpret_cast<void*>(checkpointer->id()));
Key Files
src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp— main implementationsrc/xrpld/app/main/DBInit.h— schema definitionssrc/xrpld/core/detail/DatabaseCon.cpp— kept for historical reference; lifecycle now inlibxrplsrc/libxrpl/rdb/DatabaseCon.cpp— connection lifecycle,CheckpointersCollection, destructor drainsrc/libxrpl/rdb/SociDB.cpp— SOCI/SQLite adapter,WALCheckpointer, blob conversion, memory statssrc/xrpld/app/rdb/backend/detail/Node.cpp— ledger/tx operationssrc/xrpld/app/rdb/detail/State.cpp— deletion state tracking