Repository navigation
fix(tests): order test spans by start sequence, not by started_at alone - #4392
Conversation
`SpanProcessorForTests.get_ordered_spans` sorted only on `started_at`, which comes from `datetime.now()`. On Windows before Python 3.13 that clock advances in ~15ms steps, so a parent span and its child routinely carry the identical timestamp. The sort is stable, so a tie falls back to insertion order, and spans are inserted in `on_span_end` order: a child ends before its parent, so the child came out first. `fetch_normalized_spans` builds the tree by looking up each span's parent, so a child arriving first raised `KeyError: (trace_id, span_id)`. That took out 37 tests across the tracing, MCP-tracing and runtime-symmetry suites on a clean checkout. `on_span_start` already fires for every span and was discarding it, so record the start sequence there and use it to break timestamp ties. Timestamp order is still primary, so nothing changes where the clock has enough resolution to separate spans. The regression test freezes `time_iso` instead of relying on a coarse clock, so it reproduces the ordering deterministically on every platform. (cherry picked from commit b3c5713)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30da5c9595
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Thanks for the detailed reproduction. Before merge, please key _start_order by (span.trace_id, span.span_id) rather than span_id alone, and add a tied-clock regression test with two traces that reuse an explicit span ID.
span_id is only unique within a trace, and the test helper already treats span identity as (trace_id, span_id). With the current head, the later trace can inherit the earlier trace's sequence, causing its child to remain ahead of its parent and fetch_normalized_spans() to raise KeyError. After that focused fix and regression test, this should be ready for another review.
A span ID is only unique within its trace and `span_id` is a caller-settable parameter on every span factory, so recording start order per span ID alone let a later trace inherit an earlier trace's position. Reproduced: trace A uses an explicit span ID for a span that starts early, trace B reuses it for a span that starts late. B's child kept A's index, sorted ahead of its own parent, and `fetch_normalized_spans()` raised KeyError on B's parent. The helper already treats span identity as `(trace_id, span_id)` when it builds the tree, so key the start order the same way. The parent-first assertion in the regression tests now compares on the pair too.
|
You are right, and it fails exactly the way you described. Reproduced it before changing anything, since "span IDs collide" sounded unlikely until I checked that def agent_span(name: str, ..., span_id: str | None = None) -> Span[AgentSpanData]:First trace uses an explicit ID for a span that starts early, second reuses it for one that starts late, clock frozen so every
Pushed in
The new test is discriminating rather than incidental: reverting only the keying, leaving everything else in place, fails that one test and leaves the other two green.
Worth noting the branch is a few commits behind |
On a clean checkout on Windows with Python 3.10, 3.11 or 3.12,
pytestfails 37 tests across the tracing, MCP-tracing and runtime-symmetry suites, all with the same error:Nothing is wrong with the library. The test helper's span ordering depends on clock resolution.
Mechanism
Three steps, each verifiable on its own.
1.
started_atties. It comes fromdatetime.now()viaagents.tracing.util.time_iso. On Windows the resolution of that call changed in Python 3.13, measured on this machine over a 1000-call burst:So before 3.13, a parent span and the child it opens almost always carry the identical
started_at.2. A tie resolves to the wrong order.
get_ordered_spanssorted on that field alone:sortedis stable, so a tie preserves insertion order, and spans are inserted inon_span_end. A child ends before its parent, so a tie puts the child first, which is the exact reverse of what the consumer needs.3. The consumer requires parent-first.
fetch_normalized_spanslooks the parent up while attaching each span:Reduced to three spans, with nothing else running:
Why CI does not catch it
tests-windowsruns the full suite, but pinspython-version: "3.13", the one version on Windows where the clock is fine. The Linux matrix covers 3.10 through 3.14 and is unaffected becausedatetime.now()there has microsecond resolution. So the gap is Windows on any supported version below 3.13, which no job covers.I have not touched the workflow matrix in this PR; widening it is a separate decision and this fix stands on its own.
Fix
on_span_startalready fires for every span and was discarding it. Record the start sequence there and use it only to break timestamp ties:Timestamp order stays primary, so behaviour is unchanged wherever the clock already separates spans. The map is cleared in
clear()alongside the other per-test state.Verification
Same machine, same checkout, the six affected test files:
Whole suite on Python 3.12 goes from 42 failed / 5620 passed to 5 failed / 5657 passed. The 5 that remain are unrelated to span ordering, they fail with and without this change, and some are flaky between runs; I have not chased them here.
ruff check,ruff format --checkandmypyare clean on both files.The regression test
tests/tracing/test_span_ordering.pyfreezestime_isorather than relying on a coarse clock, so it reproduces the ordering deterministically on every platform instead of only where the bug happens to bite. Verified red before green: both cases fail withKeyErroragainst the unfixed helper on Python 3.13, where the real bug cannot otherwise be observed at all.One case asserts the ordering contract directly (no span precedes its parent) and the other asserts the tree
fetch_normalized_spansbuilds from it, so a future change that satisfies one but not the other still fails.