The hierarchy check searched the parent span and inspected the three newest
traces it returned. That is wrong whenever the child is conditional on a state
the workload only sometimes reaches: the parent fires constantly, so its newest
traces are the ones LEAST likely to carry a rare child. Three relationships had
been skipped as unassertable for exactly this, and in none of them was the child
missing -- each emitted traces of its own and simply was not in the three most
recent parent traces.
The check now issues a second query, a TraceQL trace-level conjunction of the
parent and child name predicates, and inspects those traces. Tempo searches its
whole retention for co-occurrence instead of leaving the answer to which traces
happen to be newest. The parent-only query is kept and still runs first, so "the
parent stopped being emitted" stays a distinct failure from "the parent is there
but the child never co-occurs" -- they mean different things to whoever reads the
report, and collapsing them would lose that.
The returned traces are still verified with _span_name_matches rather than the
query result being trusted on its own. Tempo has already guaranteed
co-occurrence, so this is redundant on the happy path; it is kept because it
keeps the glob semantics in one place and means a wrongly built query cannot
silently pass.
_traceql_name_predicate handles the wildcard contracts. TraceQL has no glob
operator, so `rpc.command.*` is sent as name=~"rpc\.command\..*" with the dots
escaped -- unescaped they would match any character in those positions, which is
the looseness _span_name_matches exists to avoid.
Two entries follow from the fix. txq.accept -> txq.accept_tx is asserted again:
its child is created inside the queued-transaction loop behind
`if (feeLevelPaid >= requiredFeeLevel)` (TxQ.cpp:1530) while the parent fires on
every close (:1499), which was the whole reason it failed. txq.enqueue ->
txq.batch_clear stays skipped but for ONE reason now instead of two -- its child
never fires at all under this workload, needing an account with a supersedable
batch, so it is purely a workload gap and needs nothing further from the
validator. The third, ledger.acquire -> ledger.acquire.txtree, lives on the
sync-diagnostics branch and is un-skipped there once this merges forward.
Written test-first, and the first test this module has had. The failing test
reproduces the exact CI message, "txq.accept_tx not found in txq.accept traces",
against a stubbed Tempo whose corpus holds the child only in a trace outside the
newest three. Three sibling tests guard the ways this could be "fixed" wrongly: an
absent child must still fail, a missing parent must still name the parent rather
than the child, and a wildcard child must be satisfied by any family member. The
stub records the queries issued, so the conjunction is asserted rather than
assumed. A stub rather than a live Tempo because the behaviour under test is which
traces the check ASKS FOR -- a passing query against real data proves the data
co-operated, not that the query was right.
The first run of those tests failed for the wrong reason: my stub's name-predicate
regex also matched the resource.service.name="xrpld" term every query carries and
so demanded a span literally named "xrpld". Fixed in the stub, with the lookbehind
commented as load-bearing, before touching production code.
Verification: 4/4 tests pass, and the failing one was watched failing first with
the production message; the issued queries were printed and confirmed to contain
the conjunction; validate_telemetry.py compiles; expected_spans.json parses;
21 relationships, 16 asserted and 5 skipped; counters still 41 span types;
otel-naming exits 0. Three unrelated files in this worktree are another party's
live work and were deliberately left unstaged.