| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
SGPSyncTracingProcessor and SGPAsyncTracingProcessor accumulated spans in self._spans dict on every request but never removed them, since on_span_end() used dict.get() (read-only) instead of dict.pop() (read-and-remove). The only cleanup was in shutdown() which is never called. After this fix, spans are removed from the dict when they complete, preventing unbounded memory growth.
AsyncTrace.start_span() and end_span() previously awaited processor HTTP calls inline, adding 8 blocking round-trips per request. This moves processor calls into a background FIFO queue so callers enqueue and return immediately. - Add AsyncSpanQueue with sequential drain loop, error logging, and graceful shutdown with configurable timeout - Wire shutdown into FastAPI lifespan teardown in base_acp_server - FIFO ordering preserves start-before-end invariant required by SGPAsyncTracingProcessor's internal _spans dict
@patch decorators on _make_processor expired before test bodies ran, so on_span_start/on_span_end hit the real create_span and flush. Refactored to @staticmethod with 'with patch(...)' context managers matching the async test class pattern.
Use explicit dict[str, object] annotation to avoid invariance error when assigning to Span.output (Dict[str, object] | ... | None).
Processors like SGPAsyncTracingProcessor mutate span.data in-place via _add_source_to_span. With async background processing, this raced with the caller who holds a reference to the same span object. Deep-copying via model_copy(deep=True) decouples the two.
| Back | FazBrowse Home | New Git URL |
Summary
Problem
Users reported high latency with tracing enabled. Datadog showed significant time spent in /v5/spans/batch — every span start/end awaited HTTP calls to tracing processors inline. With 4 spans per request and 2 events each, that's 8 blocking network round-trips per request just for telemetry.
Solution
New AsyncSpanQueue class in span_queue.py with:
Test plan
Greptile Summary
This PR removes 8 blocking HTTP round-trips per request by introducing AsyncSpanQueue — a background FIFO drain task that processes span start/end events asynchronously while preserving the start-before-end ordering required by the SGP processor. Graceful drain-before-exit is wired into the FastAPI lifespan via shutdown_default_span_queue.
The previous concern about a shared mutable Span reference racing with the background drain task has been resolved: both start_span and end_span now enqueue a span.model_copy(deep=True), fully decoupling the live span object from the background processor.
Key observations:
Confidence Score: 5/5
Safe to merge; all remaining findings are non-blocking P2 style/hardening suggestions.
No P0 or P1 issues found. The core race condition from the previous review round is addressed by deep-copying spans before enqueue. Shutdown sequencing, FIFO ordering, and error isolation are all correctly implemented. The three P2 comments (unbounded queue, off-by-one warning count, duplicated magic number) are improvements worth making but do not affect correctness or production safety of the feature.
src/agentex/lib/core/tracing/span_queue.py — review the unbounded queue and shutdown warning accuracy.
Important Files Changed
Sequence Diagram
sequenceDiagram participant H as Request Handler participant AT as AsyncTrace participant Q as AsyncSpanQueue participant DL as Drain Task (bg) participant P as TracingProcessor H->>AT: await start_span(name) AT->>AT: build Span + model_copy(deep=True) AT->>Q: enqueue(START, span_copy, processors) Q->>Q: _ensure_drain_running() Q-->>H: return (non-blocking) AT-->>H: return live Span Note over DL,P: Background drain loop DL->>Q: await queue.get() Q-->>DL: _SpanQueueItem(START, span_copy) DL->>P: on_span_start(span_copy) P-->>DL: (HTTP call completes) DL->>Q: task_done() H->>AT: await end_span(span) AT->>AT: snapshot + model_copy(deep=True) AT->>Q: enqueue(END, span_copy, processors) Q-->>H: return (non-blocking) DL->>Q: await queue.get() Q-->>DL: _SpanQueueItem(END, span_copy) DL->>P: on_span_end(span_copy) P-->>DL: (HTTP call completes) DL->>Q: task_done() Note over Q,DL: Graceful shutdown (FastAPI lifespan teardown) Q->>Q: _stopping = True Q->>Q: await queue.join() [timeout=30s] Q->>DL: cancel() DL-->>Q: CancelledError (caught)Reviews (2): Last reviewed commit: "fix(tracing): Deep-copy spans before enq..." | Re-trigger Greptile
Context used:
Learnt From
scaleapi/scaleapi#126388