From 97da89c3e90f598e1554fbd326ef2960eebf34ed Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:10:17 +0100 Subject: [PATCH] fix(telemetry): detach consensus receive spans and root standalone fallbacks The consensus.proposal.receive / consensus.validation.receive guards are moved into checkPropose/checkValidation jobs, so detach their Scope on the peer thread before the job hand-off. The standalone (no-propagated-context) fallbacks in ConsensusReceiveTracing now use rootSpan() so they start a fresh trace root instead of inheriting an ambient span; the hashSpan propagated-context branches are unchanged (cross-node linking preserved). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/xrpld/overlay/detail/PeerImp.cpp | 10 ++++-- src/xrpld/telemetry/ConsensusReceiveTracing.h | 31 ++++++++++--------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 4b2a7e765c..dc7b88b295 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1843,7 +1843,10 @@ PeerImp::onMessage(std::shared_ptr const& m) // Create a receive span that links to the sender's trace context // (if propagated). shared_ptr keeps it alive across the job boundary. - auto span = std::make_shared(telemetry::proposalReceiveSpan(set)); + // Detach the guard's Scope on this peer thread so it is not popped on the + // job worker thread (which would leak this thread's context stack). + auto span = + std::make_shared(telemetry::proposalReceiveSpan(set).detached()); span->setAttribute(telemetry::consensus::span::attr::proposalTrusted, isTrusted); span->setAttribute( telemetry::consensus::span::attr::round, static_cast(set.proposeseq())); @@ -2438,7 +2441,10 @@ PeerImp::onMessage(std::shared_ptr const& m) // Create a receive span that links to the sender's trace context // (if propagated). shared_ptr keeps it alive across the job boundary. - auto span = std::make_shared(telemetry::validationReceiveSpan(*m)); + // Detach the guard's Scope on this peer thread so it is not popped on + // the job worker thread (which would leak this thread's context stack). + auto span = + std::make_shared(telemetry::validationReceiveSpan(*m).detached()); span->setAttribute(telemetry::consensus::span::attr::validationTrusted, isTrusted); if (val->isFieldPresent(sfLedgerSequence)) { diff --git a/src/xrpld/telemetry/ConsensusReceiveTracing.h b/src/xrpld/telemetry/ConsensusReceiveTracing.h index 8a98e3cc39..83e6db0774 100644 --- a/src/xrpld/telemetry/ConsensusReceiveTracing.h +++ b/src/xrpld/telemetry/ConsensusReceiveTracing.h @@ -1,6 +1,7 @@ #pragma once -/** Helper functions for creating consensus receive trace spans. +/** + * Helper functions for creating consensus receive trace spans. * * Encapsulates the logic for creating SpanGuard instances for incoming * proposal and validation messages with optional protobuf parent @@ -18,8 +19,8 @@ * +--- has trace_context? ----+ * | yes | no * v v - * SpanGuard::span() with SpanGuard::span() - * extracted parent context (standalone span) + * SpanGuard::hashSpan() with SpanGuard::rootSpan() + * extracted parent context (fresh trace root) * * When XRPL_ENABLE_TELEMETRY is not defined, the functions return * no-op SpanGuard instances (zero overhead, zero dependencies). @@ -31,7 +32,7 @@ * span.setAttribute(...); * @endcode * - * @note Span names come from the canonical constants in + * @note Span names come from the canonical constants in * ConsensusSpanNames.h (consensus::span::proposalReceive / * validationReceive) so they stay in sync with the rest of Phase 4. */ @@ -47,14 +48,15 @@ namespace xrpl::telemetry { -/** Create a "consensus.proposal.receive" span for an incoming proposal. +/** + * Create a "consensus.proposal.receive" span for an incoming proposal. * * If the message carries a TraceContext with a valid span_id, the * receive span is created with the sender's context as parent. * Otherwise a standalone span is created. * - * @param msg The incoming TMProposeSet protobuf message. - * @return An active SpanGuard, or a null guard if tracing is disabled. + * @param msg The incoming TMProposeSet protobuf message. + * @return An active SpanGuard, or a null guard if tracing is disabled. */ inline SpanGuard proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) @@ -82,19 +84,20 @@ proposalReceiveSpan([[maybe_unused]] protocol::TMProposeSet const& msg) } } #endif - // No propagated context — create a standalone span. - return SpanGuard::span( + // No propagated context — start a fresh trace root (not an ambient child). + return SpanGuard::rootSpan( TraceCategory::Consensus, seg::consensus, consensus::span::op::proposalReceive); } -/** Create a "consensus.validation.receive" span for an incoming validation. +/** + * Create a "consensus.validation.receive" span for an incoming validation. * * If the message carries a TraceContext with a valid span_id, the * receive span is created with the sender's context as parent. * Otherwise a standalone span is created. * - * @param msg The incoming TMValidation protobuf message. - * @return An active SpanGuard, or a null guard if tracing is disabled. + * @param msg The incoming TMValidation protobuf message. + * @return An active SpanGuard, or a null guard if tracing is disabled. */ inline SpanGuard validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) @@ -119,8 +122,8 @@ validationReceiveSpan([[maybe_unused]] protocol::TMValidation const& msg) } } #endif - // No propagated context — create a standalone span. - return SpanGuard::span( + // No propagated context — start a fresh trace root (not an ambient child). + return SpanGuard::rootSpan( TraceCategory::Consensus, seg::consensus, consensus::span::op::validationReceive); }