Merge branch 'pratik/otel-phase2-rpc-tracing' into pratik/otel-phase3-tx-tracing

# Conflicts:
#	src/libxrpl/telemetry/SpanGuard.cpp
This commit is contained in:
Pratik Mankawde
2026-07-20 21:41:45 +01:00
3 changed files with 172 additions and 0 deletions

View File

@@ -166,6 +166,7 @@
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <string_view>
namespace xrpl::telemetry {
@@ -501,6 +502,43 @@ public:
operator bool() const;
};
// --- Detach-in-place helpers -----------------------------------------------
/**
* Detach an active `std::optional<SpanGuard>` member in place.
*
* Equivalent to `guard.emplace(std::move(*guard).detached())`, which is the
* required idiom for detaching a live guard stored in an `optional` (move
* assignment is deleted because the underlying Scope cannot be re-scoped in
* place). No-op if `guard` is empty or already null.
*
* @param guard The optional guard to detach. Must be called on the thread
* that constructed the active guard inside it (same rule as
* `SpanGuard::detached()`).
* @note Must run BEFORE any `captureContext()` snapshot is taken if the
* caller also needs the pre-detach context — capture first, then call
* this helper (same ordering rule `detached()` itself has).
*/
void
detachInPlace(std::optional<SpanGuard>& guard);
/**
* Detach an active `std::shared_ptr<SpanGuard>`, returning the detached
* guard as a new `shared_ptr`.
*
* Equivalent to
* `guard = std::make_shared<SpanGuard>(std::move(*guard).detached())`.
* No-op (returns the input unchanged) if `guard` is null or points at a
* null guard.
*
* @param guard The guard to detach, taken by value (the caller's pointer
* is consumed; assign the return value back).
* @return A `shared_ptr` to the detached guard (new allocation), or the
* input pointer unchanged if it was null/inactive.
*/
[[nodiscard]] std::shared_ptr<SpanGuard>
detachInPlace(std::shared_ptr<SpanGuard> guard);
// ---------------------------------------------------------------------------
// No-op stub (all inline, zero overhead, no OTel dependency)
// ---------------------------------------------------------------------------
@@ -637,6 +675,19 @@ public:
}
};
// --- Detach-in-place helpers (no-op stubs) ---------------------------------
inline void
detachInPlace(std::optional<SpanGuard>&)
{
}
[[nodiscard]] inline std::shared_ptr<SpanGuard>
detachInPlace(std::shared_ptr<SpanGuard> guard)
{
return guard;
}
#endif // XRPL_ENABLE_TELEMETRY
} // namespace xrpl::telemetry

View File

@@ -333,6 +333,24 @@ SpanGuard::detached() &&
return SpanGuard(std::make_unique<Impl>(std::move(s), Impl::Detached{}));
}
// ===== Detach-in-place helpers =============================================
void
detachInPlace(std::optional<SpanGuard>& guard)
{
if (!guard || !*guard)
return;
guard.emplace(std::move(*guard).detached());
}
std::shared_ptr<SpanGuard>
detachInPlace(std::shared_ptr<SpanGuard> guard)
{
if (!guard || !*guard)
return guard;
return std::make_shared<SpanGuard>(std::move(*guard).detached());
}
// ===== Hash-derived span (category-gated) ==================================
SpanGuard

View File

@@ -35,6 +35,7 @@
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
@@ -414,6 +415,108 @@ TEST_F(SpanGuardScopeTest, non_detached_cross_thread_corrupts_origin_stack)
EXPECT_EQ(after->GetTraceId(), build->GetTraceId());
}
// detachInPlace(optional&) must detach the live guard the same way the
// hand-rolled emplace(std::move(*opt).detached()) idiom does: after the call
// the guard can be moved to and ended on a worker thread without leaving the
// origin thread's context stack holding it.
TEST_F(SpanGuardScopeTest, detach_in_place_optional_detaches_active_guard)
{
// Scoped optional guard is active on this (origin) thread.
std::optional<SpanGuard> opt = SpanGuard::span(TraceCategory::Ledger, "ledger", "build");
ASSERT_TRUE(opt.has_value());
ASSERT_TRUE(static_cast<bool>(*opt));
// Strip the thread-local Scope here on the origin thread via the helper.
detachInPlace(opt);
ASSERT_TRUE(opt.has_value());
ASSERT_TRUE(static_cast<bool>(*opt));
// End the detached span on a worker thread.
std::thread worker([d = std::move(*opt)]() mutable {});
worker.join();
// Back on the origin thread: a new span must be a fresh root, proving the
// origin stack top was restored by detachInPlace() (not left holding
// ledger.build).
{
auto after = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(after));
}
auto spans = spanData()->GetSpans();
auto* build = findSpan(spans, "ledger.build");
auto* after = findSpan(spans, "rpc.command");
ASSERT_NE(build, nullptr);
ASSERT_NE(after, nullptr);
// 'after' is a new root: zero parent and a different trace than
// ledger.build.
EXPECT_FALSE(after->GetParentSpanId().IsValid());
EXPECT_NE(after->GetTraceId(), build->GetTraceId());
}
// detachInPlace(optional&) on an empty optional is a no-op: it must not crash,
// must leave the optional empty, and must export nothing.
TEST_F(SpanGuardScopeTest, detach_in_place_optional_noop_on_empty)
{
std::optional<SpanGuard> opt;
ASSERT_FALSE(opt.has_value());
detachInPlace(opt);
// Still empty, no span was created or exported.
EXPECT_FALSE(opt.has_value());
EXPECT_TRUE(spanData()->GetSpans().empty());
}
// detachInPlace(shared_ptr) must detach the live guard and return it as a new
// shared_ptr, so the returned guard can be moved to and ended on a worker
// thread without corrupting the origin thread's context stack.
TEST_F(SpanGuardScopeTest, detach_in_place_shared_detaches_active_guard)
{
// Scoped shared guard is active on this (origin) thread.
auto span =
std::make_shared<SpanGuard>(SpanGuard::span(TraceCategory::Ledger, "ledger", "build"));
ASSERT_NE(span, nullptr);
ASSERT_TRUE(static_cast<bool>(*span));
// Strip the thread-local Scope here on the origin thread via the helper.
span = detachInPlace(std::move(span));
ASSERT_NE(span, nullptr);
ASSERT_TRUE(static_cast<bool>(*span));
// End the detached span on a worker thread.
std::thread worker([d = std::move(span)]() mutable {});
worker.join();
// Back on the origin thread: a new span must be a fresh root, proving the
// origin stack top was restored by detachInPlace() (not left holding
// ledger.build).
{
auto after = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(after));
}
auto spans = spanData()->GetSpans();
auto* build = findSpan(spans, "ledger.build");
auto* after = findSpan(spans, "rpc.command");
ASSERT_NE(build, nullptr);
ASSERT_NE(after, nullptr);
// 'after' is a new root: zero parent and a different trace than
// ledger.build.
EXPECT_FALSE(after->GetParentSpanId().IsValid());
EXPECT_NE(after->GetTraceId(), build->GetTraceId());
}
// detachInPlace(shared_ptr) on a null pointer is a no-op: it must not crash and
// must return null unchanged.
TEST_F(SpanGuardScopeTest, detach_in_place_shared_noop_on_null)
{
auto result = detachInPlace(std::shared_ptr<SpanGuard>{});
EXPECT_EQ(result, nullptr);
}
} // namespace
} // namespace xrpl::telemetry