Skip to content

Commit 848212a

Browse files
fix(tracing): keep eval span id captures apart from plain span pins so a span flood cannot evict them
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 3e31b85 commit 848212a

2 files changed

Lines changed: 24 additions & 5 deletions

File tree

‎src/agentex/lib/core/tracing/sgp_evals.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
_attrs_by_task: OrderedDict[str, dict[str, Any]] = OrderedDict()
3737
# Attrs captured when a span starts, so a later registry change cannot alter a span still queued for export.
3838
_attrs_by_span: OrderedDict[str, dict[str, Any]] = OrderedDict()
39+
# Spans pinned with no attrs live apart so a flood of plain spans cannot evict an eval span's pinned ids.
40+
_plain_spans: OrderedDict[str, None] = OrderedDict()
3941
_lock = threading.Lock()
4042

4143

@@ -83,24 +85,29 @@ def _lookup(span: Span) -> dict[str, Any]:
8385
def capture_for_span(span: Span) -> None:
8486
"""Pin the span's attrs at start, empty included, so an eval task registered later cannot claim it."""
8587
with _lock:
86-
_attrs_by_span[span.id] = _lookup(span)
87-
while len(_attrs_by_span) > _MAX_TASKS:
88-
_attrs_by_span.popitem(last=False)
88+
attrs = _lookup(span)
89+
store: OrderedDict[str, Any] = _attrs_by_span if attrs else _plain_spans
90+
store[span.id] = attrs or None
91+
while len(store) > _MAX_TASKS:
92+
store.popitem(last=False)
8993

9094

9195
def release_span(span_id: str) -> None:
92-
if _attrs_by_span:
96+
if _attrs_by_span or _plain_spans:
9397
with _lock:
9498
_attrs_by_span.pop(span_id, None)
99+
_plain_spans.pop(span_id, None)
95100

96101

97102
def attrs_for_span(span: Span) -> dict[str, Any]:
98103
"""Attrs captured at span start, else those of the task found by ``span.task_id`` then ``span.trace_id``."""
99-
if not _attrs_by_task and not _attrs_by_span:
104+
if not _attrs_by_task and not _attrs_by_span and not _plain_spans:
100105
return {}
101106
with _lock:
102107
if span.id in _attrs_by_span:
103108
return dict(_attrs_by_span[span.id])
109+
if span.id in _plain_spans:
110+
return {}
104111
return _lookup(span)
105112

106113

@@ -109,3 +116,4 @@ def clear() -> None:
109116
with _lock:
110117
_attrs_by_task.clear()
111118
_attrs_by_span.clear()
119+
_plain_spans.clear()

‎tests/lib/core/tracing/test_sgp_evals.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ def test_captures_are_bounded(self) -> None:
201201
assert sgp_evals.attrs_for_span(spans[0]) == {}
202202
assert sgp_evals.attrs_for_span(spans[2]) == EXPECTED_ATTRS
203203

204+
def test_plain_spans_cannot_evict_an_eval_span_capture(self) -> None:
205+
sgp_evals.register_task_metadata("task-1", EVAL_METADATA)
206+
eval_span = _span(data={})
207+
sgp_evals.capture_for_span(eval_span)
208+
sgp_evals.unregister_task("task-1")
209+
with patch.object(sgp_evals, "_MAX_TASKS", 2):
210+
for _ in range(5):
211+
sgp_evals.capture_for_span(_span(data={}))
212+
213+
assert sgp_evals.attrs_for_span(eval_span) == EXPECTED_ATTRS
214+
204215
def test_sync_trace_start_span_captures_ids(self) -> None:
205216
sgp_evals.register_task_metadata("task-1", EVAL_METADATA)
206217
span = Trace(processors=[], client=Mock(), trace_id="task-1").start_span(name="s")

0 commit comments

Comments
 (0)