feat: Add experimental async transport (port of PR #4572)#5646
Open
feat: Add experimental async transport (port of PR #4572)#5646
Conversation
Contributor
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨Langchain
Other
Bug Fixes 🐛Ci
Openai
Other
Internal Changes 🔧Langchain
Other
Other
🤖 This preview updates automatically when you update the PR. |
Contributor
Codecov Results 📊✅ 2148 passed | ⏭️ 153 skipped | Total: 2301 | Pass Rate: 93.35% | Execution Time: 6m 54s All tests are passing successfully. ✅ Patch coverage is 90.55%. Project has 12116 uncovered lines. Files with missing lines (7)
Generated by Codecov Action |
Contributor
Codecov Results 📊Generated by Codecov Action |
Add an experimental async transport using httpcore's async backend,
enabled via `_experiments={"transport_async": True}`.
This is a manual port of PR #4572 (originally merged into `potel-base`)
onto the current `master` branch.
Key changes:
- Refactor `BaseHttpTransport` into `HttpTransportCore` (shared base) +
`BaseHttpTransport` (sync) + `AsyncHttpTransport` (async, conditional
on httpcore[asyncio])
- Add `Worker` ABC and `AsyncWorker` using asyncio.Queue/Task
- Add `close_async()` / `flush_async()` to client and public API
- Patch `loop.close` in asyncio integration to flush before shutdown
- Add `is_internal_task()` ContextVar to skip wrapping Sentry-internal tasks
- Add `asyncio` extras_require (`httpcore[asyncio]==1.*`)
- Widen anyio constraint to `>=3,<5` for httpx and FastAPI
Refs: GH-4568
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8c808bf to
4f8a00c
Compare
The base class _make_pool returns a union of sync and async pool types, so mypy sees _pool.request() as possibly returning a non-awaitable. Add type: ignore[misc] since within AsyncHttpTransport the pool is always an async type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The asyncio extra on httpcore pulls in anyio, which conflicts with starlette's anyio<4.0.0 pin and causes pip to downgrade httpcore to 0.18.0. That old version crashes on Python 3.14 due to typing.Union not having __module__. Keep httpcore[http2] in requirements-testing.txt (shared by all envs) and add httpcore[asyncio] only to linters, mypy, and common envs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AsyncWorker.kill() now calls self._task.cancel() before clearing the reference, preventing duplicate consumers if submit() is called later - close() with AsyncHttpTransport now does best-effort sync cleanup (kill transport, close components) instead of silently returning - flush()/close() log warnings instead of debug when async transport used - Add __aenter__/__aexit__ to _Client for 'async with' support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Asyncio and gevent don't mix — async tests using asyncio.run() fail under gevent's monkey-patching. Add skip_under_gevent decorator to all async tests in test_transport.py and test_client.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Python 3.6 doesn't support PEP 563 (from __future__ import annotations). Use string-quoted annotations instead, matching the convention used in the rest of the SDK. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 77 tests covering: - AsyncWorker lifecycle (init, start, kill, submit, flush, is_alive) - AsyncWorker edge cases (no loop, queue full, cancelled tasks, pid mismatch) - HttpTransportCore methods (_handle_request_error, _handle_response, _update_headers, _prepare_envelope) - make_transport() async detection (with/without loop, integration, http2) - AsyncHttpTransport specifics (header parsing, capture_envelope, kill) - Client async methods (close_async, flush_async, __aenter__/__aexit__) - Client component helpers (_close_components, _flush_components) - asyncio integration (patch_loop_close, _create_task_with_factory) - ContextVar utilities (is_internal_task, mark_sentry_task_internal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use a sync test to test the no-running-loop path — there's genuinely no running loop in a sync test, so no mock needed and no leaked coroutines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After AsyncWorker.kill() cancels tasks, the event loop needs a tick to actually process the cancellations. Without this, pytest reports PytestUnraisableExceptionWarning for never-awaited coroutines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When kill() cancels the _target task while it's waiting on queue.get(), the CancelledError propagates through the coroutine. Without catching it, the coroutine gets garbage collected with an unhandled exception, causing pytest's PytestUnraisableExceptionWarning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Python 3.8, cancelled asyncio coroutines that were awaiting Queue.get() raise GeneratorExit during garbage collection, triggering PytestUnraisableExceptionWarning. This is a Python 3.8 asyncio limitation, not a real bug. Suppress the warning for async worker tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve tox.ini conflict: keep both httpx-latest line from master and our anyio>=3,<5 change.
Add tests that use asyncio.run() instead of @pytest.mark.asyncio to ensure coverage is properly tracked in the main thread. This covers: - AsyncWorker lifecycle (start, submit, flush, kill) - AsyncHttpTransport (creation, pool options, header parsing, capture_envelope, flush, kill) - Client async methods (close_async, flush_async, __aenter__/__aexit__) - make_transport() async detection - patch_loop_close, _create_task_with_factory - is_internal_task / mark_sentry_task_internal - Full task factory integration with internal task detection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AsyncWorker.kill(): Reset queue to None instead of putting a stale _TERMINATOR (since we now cancel the task directly, the terminator was never consumed and would break restart) - close() with async transport: Call _flush_components() to flush session flusher, log/metrics/span batchers even when sync flush is skipped - Update test to verify fresh queue creation after kill Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99e3031 to
86d6e36
Compare
Resolve conflicts in config.py and tox.ini: keep our anyio>=3,<5 change and the new jinja2 FastAPI dependency from master.
- AsyncWorker: Create fresh queue on start() instead of nullifying in kill(). This avoids the race where kill() nulls the queue before _on_task_complete can call task_done(), which would hang queue.join(). - AsyncHttpTransport._get_pool_options: Respect keep_alive option (was unconditionally adding keep-alive socket options). - close() with async transport: Call _flush_components() before cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pytest-cov doesn't track coverage from code running inside asyncio event loops. Add 70 synchronous tests that exercise async code paths using mocks instead of actual event loops. This ensures coverage is tracked in the main thread where the coverage tracer runs. Also fix: pin anyio<4 for older httpx versions (0.16, 0.20) that predate anyio 4.x compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bind the queue reference when the task is dispatched, not when the done callback fires. This prevents kill()/start() from replacing self._queue before old callbacks can call task_done(), which would corrupt the new queue's unfinished_tasks counter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sentrivana
reviewed
Mar 25, 2026
sentrivana
reviewed
Mar 26, 2026
- Remove all hasattr(transport, 'loop') checks in client.py — redundant
since AsyncHttpTransport.__init__ always sets self.loop
- Remove irrelevant lore entries from AGENTS.md (Consola, Zod, remark-lint)
- Remove duplicate and implementation-detail tests per reviewer:
- asyncio tests: keep only e2e tests (test_internal_tasks_not_wrapped,
test_loop_close_patching, test_loop_close_flushes_async_transport)
- client tests: remove _close_components/_flush_components detail tests
- transport tests: remove all sync-wrapper and mock-based coverage tests
that were not reviewable
- Remove section separator comments from test files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When httpcore[asyncio] is not installed, AsyncHttpTransport is aliased to HttpTransport. isinstance(transport, AsyncHttpTransport) would then be True for ALL transports, causing close()/flush() to skip the sync flush path. Guard with ASYNC_TRANSPORT_ENABLED flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0f3a2b2 to
1ac4196
Compare
test_loop_close_flushes_async_transport needs ASYNC_TRANSPORT_ENABLED to be True. In gevent env (no httpcore[asyncio]), the _flush function correctly skips async transport handling, so close_async is never called. Skip the test in that case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Add an experimental async transport using httpcore's async backend,
enabled via
_experiments={"transport_async": True}.This is a manual port of PR #4572 (originally merged into
potel-base)onto the current
masterbranch.Key changes
transport.py: Refactor
BaseHttpTransportintoHttpTransportCore(shared base) +
BaseHttpTransport(sync) +AsyncHttpTransport(async, conditional on
httpcore[asyncio]). Extract shared helpers:_handle_request_error,_handle_response,_update_headers,_prepare_envelope. Updatemake_transport()to detect thetransport_asyncexperiment.worker.py: Add
WorkerABC base class andAsyncWorkerimplementation using
asyncio.Queue/asyncio.Task.client.py: Add
close_async()/flush_async()with async-vs-synctransport detection. Extract
_close_components()/_flush_components().api.py: Expose
flush_async()as a public API.integrations/asyncio.py: Patch
loop.closeto flush pending eventsbefore shutdown. Skip span wrapping for internal Sentry tasks.
utils.py: Add
is_internal_task()/mark_sentry_task_internal()via ContextVar for async task filtering.
setup.py: Add
"asyncio"extras_require (httpcore[asyncio]==1.*).config.py / tox.ini: Widen anyio to
>=3,<5for httpx and FastAPI.Notes
tox.iniwas manually edited (the generation script requires afree-threaded Python interpreter). A full regeneration should be done
before merge.
Refs: GH-4568