Files
rippled/src/libxrpl/nodestore/DatabaseRotatingImp.cpp
Pratik Mankawde 22fd5e8601 feat(telemetry): make the sync board readable, and time real node writes (WP-B4)
The board and runbook had grown by append across eight work packages, so
they read in the order the work was done rather than the order a node
progresses. This is the coherence pass; it adds no new instrumentation.

- Dashboard: 52 panels regrouped from two rows into nine that follow the
  fresh-start sequence — bootstrap, peer supply, sync state, acquire and
  SHAMap fetch, job queue, quorum and publish, terminal blockers, then
  back-fill and spans collapsed since they answer conditional questions.
  Layout only: no title, query or description changed.
- Runbook: the flat step list becomes a decision tree branching on the
  observed symptom, with the amendment-block check first because it is
  terminal. Each branch names the panels, what healthy and unhealthy look
  like, and what to conclude. The existing steps are kept as the detail
  bodies.
- Reference table: every signal name re-checked against the code and every
  named panel against the board; four stale panel references fixed.
- Validation: every signal is now either asserted or covered by a note
  explaining why a five-node local cluster cannot produce it.

Also fixes the write-latency signal, which was inert on a real node: the
store duration was only recorded on the database-import path, while the two
production store implementations did not time themselves, so an ordinary
node reported a write count with no latency. Both now time the backend
write, which is the disk work this signal exists to expose. Without it the
"existing database syncs slower than a fresh one" diagnosis had no primary
signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:12:26 +01:00

242 lines
6.9 KiB
C++

#include <xrpl/nodestore/detail/DatabaseRotatingImp.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/config/BasicConfig.h>
#include <xrpl/nodestore/Backend.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/nodestore/DatabaseRotating.h>
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/nodestore/Scheduler.h>
#include <xrpl/nodestore/Types.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
namespace xrpl::NodeStore {
DatabaseRotatingImp::DatabaseRotatingImp(
Scheduler& scheduler,
int readThreads,
std::shared_ptr<Backend> writableBackend,
std::shared_ptr<Backend> archiveBackend,
Section const& config,
beast::Journal j)
: DatabaseRotating(scheduler, readThreads, config, j)
, writableBackend_(std::move(writableBackend))
, archiveBackend_(std::move(archiveBackend))
{
if (writableBackend_)
fdRequired_ += writableBackend_->fdRequired();
if (archiveBackend_)
fdRequired_ += archiveBackend_->fdRequired();
}
void
DatabaseRotatingImp::rotate(
std::unique_ptr<NodeStore::Backend>&& newBackend,
std::function<void(std::string const& writableName, std::string const& archiveName)> const& f)
{
// Pass these two names to the callback function
std::string const newWritableBackendName = newBackend->getName();
std::string newArchiveBackendName;
// Hold on to current archive backend pointer until after the
// callback finishes. Only then will the archive directory be
// deleted.
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
std::uint64_t copyForwards = 0;
{
std::scoped_lock const lock(mutex_);
archiveBackend_->setDeletePath();
oldArchiveBackend = std::move(archiveBackend_);
archiveBackend_ = std::move(writableBackend_);
newArchiveBackendName = archiveBackend_->getName();
writableBackend_ = std::move(newBackend);
copyForwards = copyForwardCount_.exchange(0, std::memory_order_relaxed);
}
if (copyForwards > 0)
{
JLOG(j_.warn()) << "Rotating: copied forward " << copyForwards
<< " archive-served reads into the writable backend "
"during the rotation window";
}
f(newWritableBackendName, newArchiveBackendName);
}
void
DatabaseRotatingImp::setRotationInFlight(bool inFlight)
{
rotationInFlight_.store(inFlight, std::memory_order_release);
JLOG(j_.debug()) << "Rotating: copy-forward on archive reads "
<< (inFlight ? "enabled" : "disabled");
}
std::string
DatabaseRotatingImp::getName() const
{
std::scoped_lock const lock(mutex_);
return writableBackend_->getName();
}
std::int32_t
DatabaseRotatingImp::getWriteLoad() const
{
std::scoped_lock const lock(mutex_);
return writableBackend_->getWriteLoad();
}
void
DatabaseRotatingImp::importDatabase(Database& source)
{
auto const backend = [&] {
std::scoped_lock const lock(mutex_);
return writableBackend_;
}();
importInternal(*backend, source);
}
void
DatabaseRotatingImp::sync()
{
std::scoped_lock const lock(mutex_);
writableBackend_->sync();
}
void
DatabaseRotatingImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t)
{
auto nObj = NodeObject::createObject(type, std::move(data), hash);
auto const backend = [&] {
std::scoped_lock const lock(mutex_);
return writableBackend_;
}();
// Time only the backend write, which is the disk work. One clock pair per
// stored object, accumulated into an atomic that the metrics gauge reads on
// its own schedule, so nothing is added to the read path or per tree node.
auto const begin = std::chrono::steady_clock::now();
backend->store(nObj);
recordStoreDuration(std::chrono::steady_clock::now() - begin);
storeStats(1, nObj->getData().size());
}
void
DatabaseRotatingImp::sweep()
{
// Nothing to do.
}
std::shared_ptr<NodeObject>
DatabaseRotatingImp::fetchNodeObject(
uint256 const& hash,
std::uint32_t,
FetchReport& fetchReport,
bool duplicate)
{
auto fetch = [&](std::shared_ptr<Backend> const& backend) {
Status status = Status::Ok;
std::shared_ptr<NodeObject> nodeObject;
try
{
status = backend->fetch(hash, &nodeObject);
}
catch (std::exception const& e)
{
JLOG(j_.fatal()) << "Exception, " << e.what();
rethrow();
}
switch (status)
{
case Status::Ok:
case Status::NotFound:
break;
case Status::DataCorrupt:
JLOG(j_.fatal()) << "Corrupt NodeObject #" << hash;
break;
default:
JLOG(j_.warn()) << "Unknown status=" << static_cast<int>(status);
break;
}
return nodeObject;
};
// See if the node object exists in the cache
std::shared_ptr<NodeObject> nodeObject;
auto [writable, archive] = [&] {
std::scoped_lock const lock(mutex_);
return std::make_pair(writableBackend_, archiveBackend_);
}();
// Try to fetch from the writable backend
nodeObject = fetch(writable);
if (!nodeObject)
{
// Otherwise try to fetch from the archive backend
nodeObject = fetch(archive);
if (nodeObject)
{
{
// Refresh the writable backend pointer
std::scoped_lock const lock(mutex_);
writable = writableBackend_;
}
// Update writable backend with data from the archive backend.
// While a rotation is in flight, ordinary (duplicate == false)
// reads served by the archive are copied forward too: the
// archive is about to be deleted, and a body canonicalized
// into the cache after the freshen getKeys() snapshot would
// otherwise survive only in RAM once the archive is dropped.
if (duplicate || rotationInFlight_.load(std::memory_order_acquire))
{
if (!duplicate)
copyForwardCount_.fetch_add(1, std::memory_order_relaxed);
writable->store(nodeObject);
}
}
}
if (nodeObject)
fetchReport.wasFound = true;
return nodeObject;
}
void
DatabaseRotatingImp::forEach(std::function<void(std::shared_ptr<NodeObject>)> f)
{
auto [writable, archive] = [&] {
std::scoped_lock const lock(mutex_);
return std::make_pair(writableBackend_, archiveBackend_);
}();
// Iterate the writable backend
writable->forEach(f);
// Iterate the archive backend
archive->forEach(f);
}
} // namespace xrpl::NodeStore