Skip to content

fix(observe): preserve asyncio.timeout awareness in async generator wrapper - #1690

Open
goingforstudying-ctrl wants to merge 5 commits into
langfuse:mainfrom
goingforstudying-ctrl:fix/async-generator-timeout-awareness
Open

fix(observe): preserve asyncio.timeout awareness in async generator wrapper#1690
goingforstudying-ctrl wants to merge 5 commits into
langfuse:mainfrom
goingforstudying-ctrl:fix/async-generator-timeout-awareness

Conversation

@goingforstudying-ctrl

@goingforstudying-ctrl goingforstudying-ctrl commented Jun 7, 2026

Copy link
Copy Markdown

@observe currently resumes every async-generator iteration in a new task. An asyncio.timeout entered inside the generator therefore targets a task that has already completed after the first yield, allowing later iterations to run past the deadline.

Resume the generator's awaitable in the consumer's existing task while running each send, throw, and close in the preserved contextvars.Context. This keeps task-bound timeouts effective and preserves context-variable values and tokens across yields, including OpenTelemetry child spans, without leaking generator context into the caller. aclose() uses the same mechanism and retains the current main branch's cleanup and span-finalization behavior.

Regression coverage includes timeouts entered inside the generator (both completion and expiry), task identity, context mutations and newly introduced context variables across yields, token reset during cleanup, cancellation, GeneratorExit delegation, explicit close, and child-span parenting/cleanup through the public @observe decorator. The timeout and task-identity regressions were confirmed failing against upstream main before applying the fix. Cancellation tests now synchronize on an event instead of assuming a fixed number of inner-task scheduling steps.

Related issue: langfuse/langfuse#13349

Validation:

  • uv sync --locked
  • uv run --frozen ruff check . — passed.
  • uv run --frozen ruff format --check langfuse/_client/observe.py tests/unit/test_observe.py — passed.
  • uv run --frozen mypy langfuse --no-error-summary — passed.
  • uv run --frozen pytest -n 2 --dist worksteal tests/unit --timeout=60 with the CI test-only environment values, Python 3.12 — 694 passed, 2 skipped.
  • uv run --python 3.10 --frozen pytest -q tests/unit/test_observe.py --timeout=20 in a separate environment — 19 passed, 2 skipped (asyncio.timeout requires Python 3.11).
  • The same observe tests on Python 3.14 — 21 passed.

Full-repository ruff format --check . reports existing formatting in tests/unit/test_media.py, which is unchanged from upstream main. E2E and live-provider suites were not run locally because they require a Langfuse server and provider credentials; the changed tracing behavior is covered by local in-memory exporter tests.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

CLAassistant commented Jun 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread tests/unit/test_observe.py Outdated
Comment thread langfuse/_client/observe.py Outdated
@goingforstudying-ctrl
goingforstudying-ctrl force-pushed the fix/async-generator-timeout-awareness branch 9 times, most recently from 0d4e480 to c486887 Compare June 16, 2026 09:06
@goingforstudying-ctrl
goingforstudying-ctrl force-pushed the fix/async-generator-timeout-awareness branch 5 times, most recently from 4342eee to dcc4e24 Compare June 25, 2026 18:09
…rapper

The _ContextPreservedAsyncGeneratorWrapper was creating a fresh
asyncio.Task for every __anext__ call (via asyncio.create_task(
context=...)).  This changed the task identity between generator
iterations, which silently broke asyncio.timeout / asyncio.timeout_at
because those context managers bind to asyncio.current_task() at
enter time and the bound task is already done by the next iteration.

Replace the asyncio.create_task wrapping with a token-based approach
that sets preserved contextvars directly on the current task.  Context
vars are task-local so they survive generator suspension points without
changing the task identity.  The same pattern is applied to aclose()
for consistency.

Closes langfuse/langfuse#13349
…r trade-off

- Assert span.ended == 1 after TimeoutError in test_async_generator_wrapper_respects_asyncio_timeout
- Expand docstring on _ContextPreservedAsyncGeneratorWrapper to note that context-var mutations across yield points are discarded between iterations
@goingforstudying-ctrl
goingforstudying-ctrl force-pushed the fix/async-generator-timeout-awareness branch from dcc4e24 to 0d4ca52 Compare June 29, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants