Skip to content

_TracedStream.__anext__'s use_span context manager raises ValueError on detach across asyncio context boundaries #276

Description

@Kamilbenkirane

Summary

_TracedStream.__anext__ (introduced in #272) wraps await self._inner.__anext__() in with use_span(self._span, end_on_exit=False):. The use_span context manager attaches a context token on enter and detaches on exit. When the awaited inner __anext__ suspends and resumes in a different asyncio Context, the detach raises:

ValueError: <Token var=<ContextVar name='current_context' default={} at 0x...> at 0x...> was created in a different Context

OTel logs this as "Failed to detach context" and a full traceback. Doesn't break functionality (spans + metrics still flow correctly), but produces noisy logs — observed ~19 occurrences during a single FastAPI chat-stream session.

Reproducer

Run a _TracedStream from a FastAPI streaming endpoint where the route handler is an async generator that yields chunks consumed by StreamingResponse. The asyncio task context the consumer runs in is different from the one that originally attached the span context inside __anext__.

Stack trace (truncated):

Failed to detach context
Traceback (most recent call last):
  File ".../opentelemetry/context/__init__.py", line 155, in detach
    _RUNTIME_CONTEXT.detach(token)
  File ".../opentelemetry/context/contextvars_context.py", line 53, in detach
    self._current_context.reset(token)
ValueError: <Token var=<ContextVar name='current_context' default={} at 0x...> at 0x...> was created in a different Context

Why it matters

  • 19 stack traces per chat session pollute logs in production-equivalent setups (Cloud Run, etc.).
  • Token-mismatch is a known OTel pattern when context-attach/detach straddles asyncio task boundaries; it's a real bug, not a benign warning.
  • Same shape as the FastAPI generator-dependency context issue we hit in chat-backend during PR refactor: add Chunk type parameter to Stream class for type-safe streaming #42 — known fix is to AVOID with-style context-attach across async-yield boundaries.

Suggested fix

Inside _TracedStream.__anext__, instead of with use_span(self._span, end_on_exit=False): wrapping await self._inner.__anext__(), attach the span as the current span explicitly via trace.set_span_in_context(span) + context.attach(ctx) only for the duration of the synchronous span-attribute mutation (TTFC, etc.) — not across the await.

Or: drop use_span(... end_on_exit=False) entirely. The _TracedStream already manages span lifecycle via _finalize; making the span "current" during the inner __anext__ is what triggers the regression. Most consumers rely on parent-via-trace_id rather than parent-via-current-span.

Discovered while validating chat-backend metric export (no impact on the metric work itself; metrics + spans both reach Cloud Monitoring/Trace correctly).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions