mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
Merge branch 'pratik/otel-phase4-consensus-tracing' into pratik/otel-phase5-docs-deployment
This commit is contained in:
176
src/test/rpc/RPCHandler_test.cpp
Normal file
176
src/test/rpc/RPCHandler_test.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/Status.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/core/Job.h>
|
||||
#include <xrpl/core/JobQueue.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Charge.h>
|
||||
#include <xrpl/resource/Consumer.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <exception>
|
||||
#include <future>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
/**
|
||||
* Checks the error a busy server reports for a request it never dispatches.
|
||||
*
|
||||
* RPCHandler_test ──doCommand()──> RPC::fillHandler()
|
||||
* │ │
|
||||
* └── fills ──> JobQueue <── reads ─┘
|
||||
*
|
||||
* An overloaded server answers rpcTOO_BUSY before it reads the command name, so
|
||||
* the request fields still hold whatever json type the client sent. Anything
|
||||
* that runs afterwards to describe the error has to cope with that and leave
|
||||
* the answer alone.
|
||||
*
|
||||
* @note Each testcase keeps one job-queue worker blocked for as long as it
|
||||
* runs, and releases it before returning.
|
||||
*/
|
||||
class RPCHandler_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* How many jobs to queue to hold the server over its overload threshold.
|
||||
* One job is dispatched straight away, so one spare keeps the waiting
|
||||
* count above the limit.
|
||||
*/
|
||||
static constexpr int kOverloadJobs = rpc::tuning::kMaxJobQueueClients + 2;
|
||||
|
||||
/**
|
||||
* Dispatches one request on an overloaded server and checks the client is
|
||||
* told the server is busy.
|
||||
*
|
||||
* @param params Request fields, in the form fillHandler() reads them.
|
||||
*/
|
||||
void
|
||||
expectTooBusy(json::Value const& params)
|
||||
{
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
auto& app = env.app();
|
||||
|
||||
// Only one job of this type runs at a time, so every job after the
|
||||
// first stays queued until the gate opens. They also sort above
|
||||
// JtClient, the priority the overload check counts from.
|
||||
std::promise<void> gate;
|
||||
std::shared_future<void> const open = gate.get_future().share();
|
||||
ScopeExit const openGate{[&gate]() { gate.set_value(); }};
|
||||
|
||||
int queued = 0;
|
||||
for (int i = 0; i < kOverloadJobs; ++i)
|
||||
{
|
||||
if (app.getJobQueue().addJob(JtSweep, "overload", [open]() { open.wait(); }))
|
||||
++queued;
|
||||
}
|
||||
BEAST_EXPECT(queued == kOverloadJobs);
|
||||
BEAST_EXPECT(app.getJobQueue().getJobCountGE(JtClient) > rpc::tuning::kMaxJobQueueClients);
|
||||
|
||||
resource::Charge loadType = resource::kFeeReferenceRpc;
|
||||
resource::Consumer consumer;
|
||||
rpc::JsonContext context{
|
||||
{.j = env.journal,
|
||||
.app = app,
|
||||
.loadType = loadType,
|
||||
.netOps = app.getOPs(),
|
||||
.ledgerMaster = app.getLedgerMaster(),
|
||||
.consumer = consumer,
|
||||
.role = Role::USER,
|
||||
.coro = {},
|
||||
.infoSub = {},
|
||||
.apiVersion = rpc::kApiVersionIfUnspecified},
|
||||
params,
|
||||
{}};
|
||||
|
||||
json::Value result;
|
||||
rpc::Status status;
|
||||
std::string thrown;
|
||||
try
|
||||
{
|
||||
status = rpc::doCommand(context, result);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
thrown = e.what();
|
||||
}
|
||||
|
||||
if (BEAST_EXPECTS(thrown.empty(), "doCommand threw: " + thrown))
|
||||
{
|
||||
BEAST_EXPECT(status.type() == rpc::Status::Type::ErrorCodeI);
|
||||
BEAST_EXPECT(status.toErrorCode() == RpcTooBusy);
|
||||
BEAST_EXPECT(result[jss::error].asString() == "tooBusy");
|
||||
BEAST_EXPECT(result[jss::error_code].asInt() == static_cast<int>(RpcTooBusy));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a well-formed request on an overloaded server. This is the control
|
||||
* for the two cases below: it shares their fixture and their assertions,
|
||||
* and differs only in that every field it sends is a string.
|
||||
*/
|
||||
void
|
||||
testRegisteredCommand()
|
||||
{
|
||||
testcase("Busy server, registered command");
|
||||
|
||||
json::Value params = json::ValueType::Object;
|
||||
params[jss::command] = "ping";
|
||||
expectTooBusy(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a request whose "method" field is not a string.
|
||||
*/
|
||||
void
|
||||
testNonStringMethod()
|
||||
{
|
||||
testcase("Busy server, method field is not a string");
|
||||
|
||||
// The HTTP path sets "command" from the outer method name it has
|
||||
// already checked, and passes the inner request object through
|
||||
// untouched, so "method" can arrive holding any json type.
|
||||
json::Value params = json::ValueType::Object;
|
||||
params[jss::command] = "ping";
|
||||
params[jss::method] = json::ValueType::Array;
|
||||
expectTooBusy(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a request whose "command" field is not a string.
|
||||
*/
|
||||
void
|
||||
testNonStringCommand()
|
||||
{
|
||||
testcase("Busy server, command field is not a string");
|
||||
|
||||
json::Value params = json::ValueType::Object;
|
||||
params[jss::command] = json::ValueType::Object;
|
||||
expectTooBusy(params);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRegisteredCommand();
|
||||
testNonStringMethod();
|
||||
testNonStringCommand();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(RPCHandler, rpc, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,12 +1,14 @@
|
||||
#include <xrpl/consensus/ConsensusSpanNames.h>
|
||||
#include <xrpl/telemetry/SpanGuard.h>
|
||||
#include <xrpl/telemetry/SpanNames.h>
|
||||
#include <xrpl/telemetry/Telemetry.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
using namespace xrpl;
|
||||
@@ -88,28 +90,44 @@ TEST(SpanGuardFactory, discard_safe_on_null)
|
||||
EXPECT_FALSE(span);
|
||||
}
|
||||
|
||||
TEST(SpanGuardFactory, consensus_close_time_attributes)
|
||||
TEST(SpanGuardFactory, consensus_accept_apply_attributes_are_inert_on_null_guard)
|
||||
{
|
||||
// Verify the consensus attribute pattern compiles and doesn't crash with a
|
||||
// null SpanGuard. Attribute keys/values use the underscore convention; the
|
||||
// canonical consensus::span constants are defined in the xrpld-level
|
||||
// ConsensusSpanNames.h, which a libxrpl test cannot include, so the keys are
|
||||
// written as literals here.
|
||||
{
|
||||
auto span = telemetry::SpanGuard::span(
|
||||
telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply");
|
||||
span.setAttribute("ledger_seq", static_cast<int64_t>(42));
|
||||
span.setAttribute("close_time_ripple_epoch_s", static_cast<int64_t>(780000000));
|
||||
span.setAttribute("close_time_correct", true);
|
||||
span.setAttribute("close_resolution_ms", static_cast<int64_t>(30000));
|
||||
span.setAttribute("consensus_state", std::string("finished"));
|
||||
span.setAttribute("proposing", true);
|
||||
span.setAttribute("round_time_ms", static_cast<int64_t>(3500));
|
||||
}
|
||||
{
|
||||
auto span = telemetry::SpanGuard::span(
|
||||
telemetry::TraceCategory::Consensus, telemetry::seg::consensus, "accept.apply");
|
||||
span.setAttribute("close_time_correct", false);
|
||||
span.setAttribute("consensus_state", std::string("moved_on"));
|
||||
}
|
||||
namespace cs = consensus::span;
|
||||
|
||||
// Nothing in this binary starts telemetry, so span() returns a null guard
|
||||
// before it even joins the name. Pinning that here says which of the
|
||||
// factory's exits produced the null guard the rest of the test relies on.
|
||||
ASSERT_EQ(Telemetry::getInstance(), nullptr);
|
||||
|
||||
// The attribute set RCLConsensus::doAccept() writes on consensus.accept.apply,
|
||||
// read from the same constants the emitter uses rather than copied as
|
||||
// literals. Both close-time outcomes are written below: the values differ,
|
||||
// the guard's inertness does not.
|
||||
auto applySpan = SpanGuard::span(TraceCategory::Consensus, seg::consensus, cs::op::acceptApply);
|
||||
ASSERT_FALSE(applySpan);
|
||||
|
||||
applySpan.setAttribute(cs::attr::ledgerSeq, static_cast<std::int64_t>(42));
|
||||
applySpan.setAttribute(cs::attr::closeTimeRippleEpochS, static_cast<std::int64_t>(780000000));
|
||||
applySpan.setAttribute(cs::attr::closeTimeCorrect, true);
|
||||
applySpan.setAttribute(cs::attr::closeResolutionMs, static_cast<std::int64_t>(30000));
|
||||
applySpan.setAttribute(cs::attr::consensusState, std::string_view{cs::val::finished});
|
||||
applySpan.setAttribute(cs::attr::proposing, true);
|
||||
applySpan.setAttribute(cs::attr::roundTimeMs, static_cast<std::int64_t>(3500));
|
||||
|
||||
// A write cannot activate a guard, so it still holds no span and hands out
|
||||
// no propagation bytes for an outgoing message to carry.
|
||||
EXPECT_FALSE(applySpan);
|
||||
EXPECT_FALSE(applySpan.getTraceBytes().valid);
|
||||
|
||||
// The consensus-failed branch writes the other value over the same two keys,
|
||||
// and reaches the same inert guard.
|
||||
auto movedOnSpan =
|
||||
SpanGuard::span(TraceCategory::Consensus, seg::consensus, cs::op::acceptApply);
|
||||
ASSERT_FALSE(movedOnSpan);
|
||||
|
||||
movedOnSpan.setAttribute(cs::attr::closeTimeCorrect, false);
|
||||
movedOnSpan.setAttribute(cs::attr::consensusState, std::string_view{cs::val::movedOn});
|
||||
|
||||
EXPECT_FALSE(movedOnSpan);
|
||||
EXPECT_FALSE(movedOnSpan.getTraceBytes().valid);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/basics/LocalValue.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/consensus/ConsensusSpanNames.h>
|
||||
#include <xrpl/telemetry/CoroAwareContextStorage.h>
|
||||
#include <xrpl/telemetry/DeterministicIdGenerator.h>
|
||||
@@ -509,9 +510,15 @@ TEST_F(SpanGuardScopeTest, scopedGuard_survives_localvalue_store_swap)
|
||||
xrpl::detail::LocalValues coroStore;
|
||||
xrpl::detail::LocalValues workerStore;
|
||||
|
||||
// Detach (do NOT delete) the fixture's active store, run on the coro store,
|
||||
// and remember the original so teardown gets it back.
|
||||
// Detach (do NOT delete) the fixture's active store and run on the coro
|
||||
// store. A failed ASSERT_* returns from the test body, so the restore must be
|
||||
// RAII or the thread pointer keeps owning a stack store that is about to die.
|
||||
// Declared after both stack stores, so it is destroyed before either of them.
|
||||
auto* saved = xrpl::detail::getLocalValues().release();
|
||||
xrpl::ScopeExit const restoreStore{[saved]() {
|
||||
xrpl::detail::getLocalValues().release();
|
||||
xrpl::detail::getLocalValues().reset(saved);
|
||||
}};
|
||||
xrpl::detail::getLocalValues().reset(&coroStore);
|
||||
|
||||
trc::SpanContext captured = trc::SpanContext::GetInvalid();
|
||||
@@ -541,11 +548,9 @@ TEST_F(SpanGuardScopeTest, scopedGuard_survives_localvalue_store_swap)
|
||||
auto afterPop = trc::GetSpan(ctx::RuntimeContext::GetCurrent());
|
||||
EXPECT_FALSE(afterPop->GetContext().IsValid());
|
||||
|
||||
// Restore (re-own) the fixture's store for teardown before any stack store
|
||||
// leaves scope, so the thread pointer never dangles.
|
||||
xrpl::detail::getLocalValues().release();
|
||||
xrpl::detail::getLocalValues().reset(saved);
|
||||
|
||||
// restoreStore re-owns the fixture's store from here on: it runs on every
|
||||
// exit path, and the checks below touch no LocalValue.
|
||||
//
|
||||
// The span ended exactly once, when the scope popped on resume.
|
||||
EXPECT_EQ(countSpans(spanData()->GetSpans(), "rpc.process"), 1u);
|
||||
}
|
||||
|
||||
@@ -280,15 +280,16 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal)
|
||||
|
||||
app_.getHashRouter().addSuppression(suppression);
|
||||
|
||||
// Inject the current thread's active span context (e.g. the consensus
|
||||
// round span) so receiving peers can link their proposal.receive span
|
||||
// as a child of this trace.
|
||||
// Inject this send span's own context, so receiving peers can parent their
|
||||
// proposal.receive span to it. Reading the ambient context instead would
|
||||
// find nothing: no span is activated on either thread that reaches here,
|
||||
// and the round span is deliberately never ambient.
|
||||
//
|
||||
// The helper injects only when a span is actually active, so a node with
|
||||
// telemetry compiled out, disabled by config, or simply not tracing this
|
||||
// round sends no TraceContext at all rather than an empty one that makes
|
||||
// every peer take its has_trace_context() branch for nothing.
|
||||
telemetry::injectCurrentContext(prop);
|
||||
// Injection writes only when the span is live, so a node with telemetry
|
||||
// compiled out, disabled by config, or simply not tracing this round sends
|
||||
// no TraceContext at all rather than an empty one that makes every peer
|
||||
// take its has_trace_context() branch for nothing.
|
||||
telemetry::injectSpanContext(span, prop);
|
||||
|
||||
app_.getOverlay().broadcast(prop);
|
||||
}
|
||||
@@ -1109,18 +1110,20 @@ RCLConsensus::Adaptor::validate(RCLCxLedger const& ledger, RCLTxSet const& txns,
|
||||
// Broadcast to all our peers:
|
||||
protocol::TMValidation val;
|
||||
val.set_validation(serialized.data(), serialized.size());
|
||||
// Inject the current thread's active span context so receiving
|
||||
// peers can link their validation.receive span as a child.
|
||||
// Inject this validation span's own context, so receiving peers can parent
|
||||
// their validation.receive span to it. Reading the ambient context instead
|
||||
// would find nothing: valSpan is parented through a stored context and is
|
||||
// never activated on this thread.
|
||||
//
|
||||
// The trace_context appended below is outside the signature on
|
||||
// `serialized`, so it is not covered by validation authenticity.
|
||||
// Downstream consumers treat it as advisory only. A signature-covered
|
||||
// trace context is a possible future enhancement.
|
||||
//
|
||||
// As on the proposal path, the helper injects only when a span is actually
|
||||
// active, so a node that is not tracing sends no TraceContext at all
|
||||
// rather than an empty one.
|
||||
telemetry::injectCurrentContext(val);
|
||||
// Injection writes only when the span is live, so a node that is not
|
||||
// tracing sends no TraceContext at all rather than an empty one.
|
||||
if (valSpan)
|
||||
telemetry::injectSpanContext(*valSpan, val);
|
||||
app_.getOverlay().broadcast(val);
|
||||
|
||||
// Publish to all our subscribers:
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
* | +-----------------------------------------------------------+ |
|
||||
* +----------------------------------------------------------------+
|
||||
*
|
||||
* pathfind.request ends with status error whenever the handler's reply
|
||||
* carries an rpc error. The description is that error's registry token,
|
||||
* never request text.
|
||||
*
|
||||
* Async recomputation (ledger close):
|
||||
*
|
||||
* +----------------------------------------------------------------+
|
||||
|
||||
@@ -158,6 +158,42 @@ fillHandler(JsonContext& context, Handler const*& result)
|
||||
return RpcSuccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Names the reason a command failed, for the span's error description.
|
||||
*
|
||||
* jss::error holds the error token, and an old-style handler reports it there
|
||||
* and nowhere else. A failure the reply does not name is described by the
|
||||
* status's own error code, which is the only reason left to report. Every
|
||||
* token is a compile-time string, so no request text reaches the description.
|
||||
*
|
||||
* @param status What the handler returned.
|
||||
* @param result The reply the handler filled in. The returned view can point
|
||||
* into it, so result must outlive the view.
|
||||
* @param replyHasError The caller's containsError(result), passed in so the
|
||||
* reply is not searched twice.
|
||||
* @return The error token, or "error" where neither source carries one.
|
||||
*/
|
||||
std::string_view
|
||||
errorDescription(Status const& status, json::Value const& result, bool replyHasError)
|
||||
{
|
||||
if (replyHasError && result[jss::error].isString())
|
||||
{
|
||||
// asCString() asserts the type, then hands back the stored pointer
|
||||
// unchecked, and a string-typed json::Value may hold a null one. Both
|
||||
// checks are needed before that pointer becomes a view.
|
||||
if (char const* const token = result[jss::error].asCString(); token != nullptr)
|
||||
return token;
|
||||
}
|
||||
|
||||
// A TER or a bare integer code has no token in the error registry, so
|
||||
// reading one would name an unrelated error. getErrorInfo() returns a
|
||||
// reference into a static table, so its token outlives this call.
|
||||
if (status.type() == Status::Type::ErrorCodeI)
|
||||
return getErrorInfo(status.toErrorCode()).token.cStr();
|
||||
|
||||
return rpc_span::val::error;
|
||||
}
|
||||
|
||||
Status
|
||||
callMethod(JsonContext& context, Handler::Method method, std::string_view name, json::Value& result)
|
||||
{
|
||||
@@ -190,23 +226,33 @@ callMethod(JsonContext& context, Handler::Method method, std::string_view name,
|
||||
JLOG(context.j.debug()) << "RPC call " << name << " completed in "
|
||||
<< ((end - start).count() / 1000000000.0) << "seconds";
|
||||
perfLog.rpcFinish(name, curId);
|
||||
span.setAttribute(rpc_span::attr::loadType, context.loadType.label().c_str());
|
||||
// Status::operator bool() returns true when there IS an error
|
||||
// (code_ != OK), so the ternary correctly maps error->error, ok->success.
|
||||
span.setAttribute(
|
||||
rpc_span::attr::rpcStatus,
|
||||
ret ? std::string_view{rpc_span::val::error}
|
||||
: std::string_view{rpc_span::val::success});
|
||||
// Reflect the result in the OTel span status, not just the attribute,
|
||||
// so non-exception RPC errors (rpcTOO_BUSY, rpcNO_PERMISSION, ...) are
|
||||
// visible to {status.code=error} queries.
|
||||
if (ret)
|
||||
// Everything in here only feeds the span, and searching the reply is
|
||||
// not free, so a null guard pays for none of it. setError() and
|
||||
// setAttribute() are no-ops on a null guard, but their arguments are
|
||||
// not: with telemetry compiled out operator bool() is a constant false.
|
||||
if (span)
|
||||
{
|
||||
span.setError(rpc_span::val::error);
|
||||
}
|
||||
else
|
||||
{
|
||||
span.setOk();
|
||||
// Read after the handler ran, because a handler may raise its own
|
||||
// load type (pathfind charges a heavy burden).
|
||||
span.setAttribute(rpc_span::attr::loadType, context.loadType.label().c_str());
|
||||
// An old-style handler reports its error in the reply, not in the
|
||||
// Status: byRef() returns a default Status whatever happened.
|
||||
// Reading both covers every handler. Status::operator bool() is
|
||||
// true when there IS an error.
|
||||
bool const replyHasError = containsError(result);
|
||||
bool const failed = static_cast<bool>(ret) || replyHasError;
|
||||
// Two values only. rpc_status is a spanmetrics dimension, so every
|
||||
// value it can take becomes a Prometheus label and a metric series.
|
||||
span.setAttribute(
|
||||
rpc_span::attr::rpcStatus,
|
||||
failed ? std::string_view{rpc_span::val::error}
|
||||
: std::string_view{rpc_span::val::success});
|
||||
// Error so a failed call answers {status.code=error}, for the codes
|
||||
// that never throw (rpcTOO_BUSY, rpcNO_PERMISSION, ...). Success
|
||||
// stays Unset: the spec reserves Ok for an operator asserting
|
||||
// verified success, and a tool may read it as suppressing errors.
|
||||
if (failed)
|
||||
span.setError(errorDescription(ret, result, replyHasError));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -234,32 +280,37 @@ callMethod(JsonContext& context, Handler::Method method, std::string_view name,
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
// Resolve the span suffix / command attribute for a request that failed in
|
||||
// fillHandler. Returns the canonical handler name for a recognized command
|
||||
// (a finite, bounded set) or the literal "unknown" for a request that omits
|
||||
// both fields or names an unregistered command. The raw request value is
|
||||
// deliberately NOT used: the command attribute is promoted to a Prometheus
|
||||
// label by the spanmetrics connector, so an attacker-controlled string would
|
||||
// let arbitrary request input drive unbounded span-name / label cardinality.
|
||||
// Resolving against the registry keeps per-command error attribution for real
|
||||
// commands (e.g. a submit rejected with rpcTOO_BUSY stays rpc.command.submit)
|
||||
// while collapsing garbage input to a single series.
|
||||
// fillHandler. The name comes from the handler registry, so only a registered
|
||||
// handler name or the "unknown" label can reach the span; request text never
|
||||
// does. That bounded set also bounds the Prometheus label the spanmetrics
|
||||
// connector derives from it, and a real command still keeps its own error
|
||||
// attribution: a submit rejected with rpcTOO_BUSY stays rpc.command.submit.
|
||||
std::string_view
|
||||
resolveCommandSpanName(JsonContext const& context)
|
||||
{
|
||||
if (!context.params.isMember(jss::command) && !context.params.isMember(jss::method))
|
||||
bool const hasCommand = context.params.isMember(jss::command);
|
||||
bool const hasMethod = context.params.isMember(jss::method);
|
||||
|
||||
if (!hasCommand && !hasMethod)
|
||||
return rpc_span::val::unknownCommand;
|
||||
|
||||
// A json array or object throws when asked for its string value, and no
|
||||
// non-string field names a handler. The reply's error code is already
|
||||
// decided, so naming the span must not be able to change it.
|
||||
if ((hasCommand && !context.params[jss::command].isString()) ||
|
||||
(hasMethod && !context.params[jss::method].isString()))
|
||||
return rpc_span::val::unknownCommand;
|
||||
|
||||
// fillHandler() rejects a request that supplies both fields with differing
|
||||
// values as rpcUNKNOWN_COMMAND. Mirror that here, or the span would be
|
||||
// labelled with one of the two names and misattribute the error to a
|
||||
// command that was never dispatched.
|
||||
if (context.params.isMember(jss::command) && context.params.isMember(jss::method) &&
|
||||
if (hasCommand && hasMethod &&
|
||||
context.params[jss::command].asString() != context.params[jss::method].asString())
|
||||
return rpc_span::val::unknownCommand;
|
||||
|
||||
std::string const cmd = context.params.isMember(jss::command)
|
||||
? context.params[jss::command].asString()
|
||||
: context.params[jss::method].asString();
|
||||
std::string const cmd = hasCommand ? context.params[jss::command].asString()
|
||||
: context.params[jss::method].asString();
|
||||
|
||||
auto const* handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, cmd);
|
||||
return (handler != nullptr) ? std::string_view{handler->name}
|
||||
|
||||
@@ -391,6 +391,10 @@ ServerHandler::onWSMessage(
|
||||
// Fresh root so each WS message is its own trace.
|
||||
auto span = ScopedSpanGuard::freshRoot(
|
||||
TraceCategory::Rpc, rpc_span::prefix::rpc, rpc_span::op::wsMessage);
|
||||
// rpc_status is a span-metrics dimension, so leaving it unset emits a
|
||||
// series with a blank label and hides this failure from any query that
|
||||
// selects on error.
|
||||
span.setAttribute(rpc_span::attr::rpcStatus, rpc_span::val::error);
|
||||
span.setError(rpc_span::val::invalidJson);
|
||||
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
|
||||
@@ -47,18 +47,28 @@ doPathFind(rpc::JsonContext& context)
|
||||
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
|
||||
}
|
||||
|
||||
// A failed reply carries the rpc error token, so reading the status off the
|
||||
// reply covers every exit, including the ones whose reply is built further
|
||||
// down the call chain. The token set is fixed by the error registry, so it
|
||||
// is safe as a span label; raw request text would not be.
|
||||
auto const finish = [&span](json::Value&& reply) -> json::Value {
|
||||
if (span && rpc::containsError(reply))
|
||||
span.setError(std::as_const(reply)[jss::error].asString());
|
||||
return std::move(reply);
|
||||
};
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
return rpcError(RpcNotSupported);
|
||||
return finish(rpcError(RpcNotSupported));
|
||||
|
||||
auto lpLedger = context.ledgerMaster.getClosedLedger();
|
||||
|
||||
if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString())
|
||||
{
|
||||
return rpcError(RpcInvalidParams);
|
||||
return finish(rpcError(RpcInvalidParams));
|
||||
}
|
||||
|
||||
if (!context.infoSub)
|
||||
return rpcError(RpcNoEvents);
|
||||
return finish(rpcError(RpcNoEvents));
|
||||
|
||||
context.infoSub->setApiVersion(context.apiVersion);
|
||||
|
||||
@@ -68,8 +78,8 @@ doPathFind(rpc::JsonContext& context)
|
||||
{
|
||||
context.loadType = resource::kFeeHeavyBurdenRpc;
|
||||
context.infoSub->clearRequest();
|
||||
return context.app.getPathRequestManager().makePathRequest(
|
||||
context.infoSub, lpLedger, context.params);
|
||||
return finish(context.app.getPathRequestManager().makePathRequest(
|
||||
context.infoSub, lpLedger, context.params));
|
||||
}
|
||||
|
||||
if (sSubCommand == "close")
|
||||
@@ -77,10 +87,10 @@ doPathFind(rpc::JsonContext& context)
|
||||
InfoSubRequest::pointer const request = context.infoSub->getRequest();
|
||||
|
||||
if (!request)
|
||||
return rpcError(RpcNoPfRequest);
|
||||
return finish(rpcError(RpcNoPfRequest));
|
||||
|
||||
context.infoSub->clearRequest();
|
||||
return request->doClose();
|
||||
return finish(request->doClose());
|
||||
}
|
||||
|
||||
if (sSubCommand == "status")
|
||||
@@ -88,12 +98,12 @@ doPathFind(rpc::JsonContext& context)
|
||||
InfoSubRequest::pointer const request = context.infoSub->getRequest();
|
||||
|
||||
if (!request)
|
||||
return rpcError(RpcNoPfRequest);
|
||||
return finish(rpcError(RpcNoPfRequest));
|
||||
|
||||
return request->doStatus(context.params);
|
||||
return finish(request->doStatus(context.params));
|
||||
}
|
||||
|
||||
return rpcError(RpcInvalidParams);
|
||||
return finish(rpcError(RpcInvalidParams));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -56,8 +56,18 @@ doRipplePathFind(rpc::JsonContext& context)
|
||||
span.setAttribute(pathfind_span::attr::destAccount, redactAccount(dst.asString()));
|
||||
}
|
||||
|
||||
// A failed reply carries the rpc error token, so reading the status off the
|
||||
// reply covers every exit, including the ones whose reply is built further
|
||||
// down the call chain. The token set is fixed by the error registry, so it
|
||||
// is safe as a span label; raw request text would not be.
|
||||
auto const finish = [&span](json::Value&& reply) -> json::Value {
|
||||
if (span && rpc::containsError(reply))
|
||||
span.setError(std::as_const(reply)[jss::error].asString());
|
||||
return std::move(reply);
|
||||
};
|
||||
|
||||
if (context.app.config().pathSearchMax == 0)
|
||||
return rpcError(RpcNotSupported);
|
||||
return finish(rpcError(RpcNotSupported));
|
||||
|
||||
context.loadType = resource::kFeeHeavyBurdenRpc;
|
||||
|
||||
@@ -73,8 +83,8 @@ doRipplePathFind(rpc::JsonContext& context)
|
||||
rpc::tuning::kMaxValidatedLedgerAge)
|
||||
{
|
||||
if (context.apiVersion == 1)
|
||||
return rpcError(RpcNoNetwork);
|
||||
return rpcError(RpcNotSynced);
|
||||
return finish(rpcError(RpcNoNetwork));
|
||||
return finish(rpcError(RpcNotSynced));
|
||||
}
|
||||
|
||||
PathRequest::pointer request;
|
||||
@@ -175,17 +185,17 @@ doRipplePathFind(rpc::JsonContext& context)
|
||||
jvResult = request->doStatus(context.params);
|
||||
}
|
||||
|
||||
return jvResult;
|
||||
return finish(std::move(jvResult));
|
||||
}
|
||||
|
||||
// The caller specified a ledger
|
||||
jvResult = rpc::lookupLedger(lpLedger, context);
|
||||
if (!lpLedger)
|
||||
return jvResult;
|
||||
return finish(std::move(jvResult));
|
||||
|
||||
rpc::LegacyPathFind const lpf(isUnlimited(context.role), context.app);
|
||||
if (!lpf.isOk())
|
||||
return rpcError(RpcTooBusy);
|
||||
return finish(rpcError(RpcTooBusy));
|
||||
|
||||
auto result = context.app.getPathRequestManager().doLegacyPathRequest(
|
||||
context.consumer, lpLedger, context.params);
|
||||
@@ -193,7 +203,7 @@ doRipplePathFind(rpc::JsonContext& context)
|
||||
for (auto& fieldName : jvResult.getMemberNames())
|
||||
result[fieldName] = std::move(jvResult[fieldName]);
|
||||
|
||||
return result;
|
||||
return finish(std::move(result));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
Reference in New Issue
Block a user