Skip to content

refactor(python-sdk): unify the pyqwest connection pools - #1659

Open
mishushakov wants to merge 1 commit into
mainfrom
python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291
Open

refactor(python-sdk): unify the pyqwest connection pools#1659
mishushakov wants to merge 1 commit into
mainfrom
python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291

Conversation

@mishushakov

@mishushakov mishushakov commented Aug 11, 2026

Copy link
Copy Markdown
Member

Every persistent HTTP stack in the Python SDK — control-plane REST, the envd HTTP API, the envd RPC clients, and the volume content API — now draws its connection pool from e2b.api.client_sync/client_async keyed on (proxy, idle read bound), instead of each caching one of its own; reqwest pools per host internally, so one pool serves the API host and every per-sandbox host without interference, and because envd RPC and the envd HTTP API hit the same host an active sandbox needs a single HTTP/2 connection instead of one per stack. Two accessors expose it (get_pyqwest_transport for connectrpc, get_httpx_transport for the generated httpx clients) while per-layer concerns stay above the pool, so PlainHTTPErrorTransport becomes a stateless per-client wrapper and Connect-error normalization stays RPC-only. Streamed downloads keep a pool of their own — the only one carrying the idle read_timeout, since reqwest's read timer runs during body send and TTFB and would otherwise cut off long uploads.

Sharing puts the sandbox health probe on the connection the failed RPC was using, so tests/test_shared_transport_pool.py pins that at the frame level with a new multi-connection HTTP/2 server serving both routes on one pool: an RST_STREAM kills only the stream and the probe reuses the same connection (which is also the proof the pool is genuinely shared), while a dropped TCP connection makes reqwest redial — both still answer, so handle_rpc_exception_with_health keeps telling a wedged connection apart from a dead sandbox.

No user-facing API change, so there are no usage examples to add — the public surface, timeouts, retry policy, and proxy handling are all unchanged, and JS has no counterpart since pyqwest pools are Python-only.

Closes SDK-291.

Notes for review

  • Template context uploads deliberately stayed on their own throwaway transport: pyqwest's retry middleware mirrors every non-bytes request body in RAM to make it replayable (verified — a 16 MiB streamed PUT buffered 16 MiB), so joining the shared retrying pool would buffer whole build contexts. That's a pre-existing bug on main for volume uploads and envd files.write — filed as SDK-332 with the repro and two verified fixes; note the obvious fix doesn't work, because gating retries on the body type would silently strip connect retries from every envd RPC (connectrpc hands pyqwest a generator even for unary calls).
  • RPC and envd HTTP now multiplex on one HTTP/2 connection and share its concurrent-stream budget (Go's default is 250, and hyper dials a second connection when one saturates) — low risk, but a real behavior change under heavy per-sandbox concurrency.

🤖 Generated with Claude Code

Every persistent HTTP stack in the SDK now draws its connection pool from
`e2b.api.client_sync`/`client_async`, keyed on (proxy, idle read bound),
instead of caching four of its own: the control-plane REST API, the envd
HTTP API, the envd RPC clients, and the volume content API. reqwest pools
per host internally, so one pool serves the API host and every per-sandbox
host without interference — and because envd RPC and the envd HTTP API hit
the same host, an active sandbox needs a single HTTP/2 connection instead
of one per stack.

Two accessors expose it: `get_pyqwest_transport` hands connectrpc the
pool behind the connect-only retries, and `get_httpx_transport` hands the
generated httpx clients the `PyqwestTransport` adapter over that same
pool. Layers above stay per-consumer, as the design calls for:
`PlainHTTPErrorTransport` is now a stateless per-client wrapper rather
than a cached transport, so Connect-error normalization stays RPC-only.
Streamed downloads keep a pool of their own — the only one carrying the
idle `read_timeout`, since reqwest's read timer runs during body send and
TTFB and would otherwise cut off long uploads.

Sharing puts the sandbox health probe on the connection the failed RPC was
using, so `tests/test_shared_transport_pool.py` pins that at the frame
level with a new multi-connection HTTP/2 server serving both routes on one
pool: an RST_STREAM kills only the stream and the probe reuses the same
connection, while a dropped TCP connection makes reqwest redial. Both
paths still answer, so `handle_rpc_exception_with_health` keeps telling a
wedged connection apart from a dead sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 11, 2026

Copy link
Copy Markdown

SDK-291

@cla-bot cla-bot Bot added the cla-signed label Aug 11, 2026
@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ad9eb51

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@e2b/python-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches all SDK HTTP/RPC wiring and changes connection multiplexing (RPC + envd HTTP on one HTTP/2 link per sandbox); behavior is heavily tested but pool sharing can affect timeouts, retries, and concurrency under load.

Overview
Persistent HTTP in the Python SDK no longer keeps separate pyqwest pools per stack. Control-plane REST, envd HTTP, envd RPC (connectrpc), and volume content all use process-global caches in e2b.api.client_sync / client_async, keyed by (proxy, idle read_timeout). get_pyqwest_transport owns the reqwest pool; get_httpx_transport is the shared httpx adapter. Streamed downloads still use a second pool when READ_TIMEOUT is set, so long uploads are not cut off by reqwest’s read timer during body send/TTFB.

Envd RPC drops its own transport cache: PlainHTTPErrorTransport wraps the shared pool per client so RPC and envd HTTP to the same sandbox can share one HTTP/2 connection. Volume modules delete duplicate pool logic and call the API helpers instead. get_envd_transport is folded into get_transport(..., for_streaming=...).

Tests add transport_caches.reset_transport_caches, assert API and envd HTTP share one transport, and add frame-level shared-pool coverage (RST_STREAM vs TCP drop) so /health after a failed RPC still works on reuse or redial.

Reviewed by Cursor Bugbot for commit ad9eb51. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 19f7310. Download artifacts from this workflow run.

JS SDK (e2b@2.38.4-python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291.0):

npm install ./e2b-2.38.4-python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291.0.tgz

CLI (@e2b/cli@2.16.2-python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291.0):

npm install ./e2b-cli-2.16.2-python-sdk-unify-pyqwest-connection-pools-once-all-http-sdk-291.0.tgz

Python SDK (e2b==2.38.0+python.sdk.unify.pyqwest.connection.pools.once.all.http.sdk.291):

pip install ./e2b-2.38.0+python.sdk.unify.pyqwest.connection.pools.once.all.http.sdk.291-py3-none-any.whl

@mishushakov
mishushakov marked this pull request as ready for review August 11, 2026 11:04

@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.

I reviewed this PR and didn't find any bugs. Because it's a large, cross-cutting refactor of the Python SDK's connection-pooling internals that changes runtime sharing behavior across the REST, envd RPC, envd HTTP, and volume stacks, a human look would still be worthwhile.

What was reviewed:

  • Cache-key correctness for the unified transport dicts (proxy, idle read bound) across sync/async and all four consumers (API, envd HTTP, envd RPC, volume).
  • The two-step lock pattern in get_httpx_transport (resolve pool, then check/populate the httpx-adapter cache) — not a race since each dict access is independently locked and the operations are idempotent.
  • New frame-level HTTP/2 tests (test_shared_transport_pool.py) validating that a health probe still succeeds after an RPC-stream reset (shared connection reuse) and after a full connection drop (redial).
Extended reasoning...

Overview

This PR consolidates four previously-separate pyqwest connection pools (control-plane REST API, envd HTTP API, envd RPC clients, and volume content API) into one process-global pool per (proxy, idle read bound) key, shared via get_pyqwest_transport/get_httpx_transport in e2b.api.client_sync/client_async. It removes per-stack transport caches from e2b/envd/client_async, e2b/envd/client_sync, and e2b/volume/client_*, replacing them with calls into the shared accessors. PlainHTTPErrorTransport becomes a stateless per-client wrapper instead of being cached alongside the pool. Streamed downloads keep a dedicated pool carrying the idle read timeout. Tests are substantially reworked: a new transport_caches.py helper resets all caches, and a new test_shared_transport_pool.py adds a real multi-connection plaintext HTTP/2 server to verify that RPC and envd HTTP now share one HTTP/2 connection per sandbox, and that health probes still succeed whether the shared connection survives (RST_STREAM) or is dropped entirely.

Security risks

None identified. No auth, crypto, or permission logic changes — this is purely about how many TCP/HTTP2 connections are opened and reused. Proxy credentials and headers remain part of the cache key (verified via existing tests), so proxy configs aren't cross-contaminated between differently-configured clients.

Level of scrutiny

This warrants a higher bar than a typical patch: it rewires the connection-pooling layer underneath every HTTP and RPC call the SDK makes (sandbox creation, file I/O, process RPC, volume access). The PR description itself flags a real behavioral change — RPC and envd HTTP now multiplex on one HTTP/2 connection and share its concurrent-stream budget, which could matter under heavy per-sandbox concurrency. That's the kind of tradeoff a maintainer with production traffic context should sign off on, even though the mechanics look correct and are well covered by tests.

Other factors

The test suite is extensive and specifically targets the riskiest part of the change (shared-connection health probing after a broken RPC stream) with a real socket-level HTTP/2 server rather than mocks, which gives good confidence in the specific scenario called out in the description. The PR also transparently documents a pre-existing, unrelated bug it deliberately does not fix (retry-buffering of streamed upload bodies), filed separately as SDK-332 — that's out of scope here and not a reason to block this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant