Skip to content

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

Merged
mishushakov merged 9 commits into
mainfrom
cursor/pr-claiming-mechanism-1180
Aug 19, 2026
Merged

refactor(python-sdk): unify the pyqwest connection pools#1692
mishushakov merged 9 commits into
mainfrom
cursor/pr-claiming-mechanism-1180

Conversation

@cursor

@cursor cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Claimed from #1659 on /sdk claim by the PR's own author (@mishushakov, org member). The original commit is carried over untouched, so authorship and the Co-Authored-By trailer are preserved — only PR ownership moves. Please close #1659 in favour of this PR (Closes does not auto-close pull requests, and this automation has no write access to do it).

Closes SDK-291.

What changes

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, HTTP version), 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.

Added while claiming

One regression test the original was missing (test_{sync,async}_closing_one_client_leaves_the_shared_pool_open in tests/test_api_client_transport.py). The refactor's docstrings promise that "closing an httpx client leaves the pool intact for the other clients on it", and that promise is now load-bearing process-wide rather than per-stack, but nothing asserted it: pyqwest pools are closable (SyncHTTPTransport.close/HTTPTransport.aclose) and every httpx client in the SDK holds the same cached adapter over one. The existing tests all close their clients inside finally and then reset the caches, so a close that reached the pool would go unnoticed.

The new tests round-trip against the local echo server, then close the control-plane client and assert that both a sibling client (the envd HTTP API) and the pool the envd RPC stack executes on directly still work. Verified in the pinned dependency that PyqwestTransport/AsyncPyqwestTransport inherit httpx's no-op close/aclose and never touch the wrapped pool, and confirmed the assertions are not vacuous: forwarding the adapter's close() to the pool makes both of them fail with RuntimeError: Executing request on already closed transport.

Verification

  • uv run pytest tests/*.py -q — 264 passed (262 before the added test).
  • uv run pytest tests/shared -q — 128 passed, 1 skipped.
  • pnpm run format, pnpm run lint, pnpm run typecheck — clean.
  • Checked the two claims from the original description that a reader would have to take on trust: pyqwest's retry middleware does mirror non-bytes request bodies in RAM (RetryingRequestContent accumulates every chunk into a bytearray to make the body replayable), which is why template context uploads deliberately keep their own non-retrying transport — and why the same buffering applies to volume uploads and envd files.write on the shared retrying pool, a pre-existing issue on main filed as SDK-332 rather than something this PR introduces.

Notes for review

  • 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.
  • get_envd_transport survives only as an alias of get_transport because external consumers (e2b-code-interpreter) call it; prefer get_transport inside the SDK.
  • The changeset from the original PR is carried over unchanged (@e2b/python-sdk patch); the added test needs none of its own.

🤖 Generated with Claude Code

Open in Web View Automation 

mishushakov and others added 2 commits August 19, 2026 15:53
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>
… pool open

The unified pool is process-global now, so a close reaching it would take
down every stack at once: pyqwest pools are closable and every httpx client
holds the same cached adapter over one. The adapter forwards neither
close() nor the context-manager exit the generated clients expose, which is
what the docstrings promise — assert it against a live server for the sync
and async pools, covering both a sibling httpx client and the pool the envd
RPC stack executes on directly.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

SDK-291

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

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 54bb596

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

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 3f2884d. Download artifacts from this workflow run.

JS SDK (e2b@2.40.1-cursor-pr-claiming-mechanism-1180.0):

npm install ./e2b-2.40.1-cursor-pr-claiming-mechanism-1180.0.tgz

CLI (@e2b/cli@2.16.3-cursor-pr-claiming-mechanism-1180.0):

npm install ./e2b-cli-2.16.3-cursor-pr-claiming-mechanism-1180.0.tgz

Python SDK (e2b==2.40.0+cursor.pr.claiming.mechanism.1180):

pip install ./e2b-2.40.0+cursor.pr.claiming.mechanism.1180-py3-none-any.whl

@mishushakov
mishushakov marked this pull request as ready for review August 19, 2026 14:13
@mishushakov
mishushakov self-requested a review as a code owner August 19, 2026 14:13

@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 refactor of the Python SDK's core HTTP transport/connection-pooling layer with a real behavior change under load (envd RPC and the envd HTTP API now share one HTTP/2 connection's stream budget per sandbox, per the PR's own notes), a human look would still be worthwhile before merging.

What was reviewed: the unified pool cache keying (proxy, idle read bound, HTTP version) across the REST, envd HTTP, envd RPC, and volume stacks; the pyqwest-pool/httpx-adapter split and that closing one httpx client doesn't close the shared pool (get_httpx_transport/adapter close semantics, backed by the new regression test); and the frame-level HTTP/2 tests verifying the health probe reuses the live connection after an RST_STREAM and redials after a dropped TCP connection.

Extended reasoning...

Overview

This PR replaces four independently-cached pyqwest connection pools (control-plane REST, envd HTTP API, envd RPC, volume content API) with one shared pool per (proxy, idle read bound, HTTP version) key, exposed via two accessors (get_pyqwest_transport for connectrpc, get_httpx_transport for httpx clients) in e2b.api.client_sync/client_async. It touches the core transport plumbing for essentially every Python SDK network call: control-plane API requests, envd file transfers, envd RPC (process/filesystem), and volume content. Test infrastructure is substantially reworked to match (a new multi-connection HTTP/2 test server, a shared transport_caches.py reset helper, and a new frame-level test file asserting connection-reuse/redial behavior around RST_STREAM and dropped TCP connections).

Security risks

No new attack surface — no auth, crypto, or permission logic changes. The retry/plain-error-normalization semantics are preserved (connect-only retries, same error mapping), just relocated. The main risk category here is correctness/availability, not security: a bug in pool-key derivation or lock handling could cause connection reuse across incompatible configurations (e.g. wrong proxy or HTTP version), but the cache key and tests appear to guard against that.

Level of scrutiny

This warrants more than a mechanical-change pass. It's a large (16-file), non-trivial refactor of shared, process-global mutable state (pool caches guarded by locks) in a foundational part of the SDK that every sandbox operation depends on. The author's own PR description flags a real behavior change: envd RPC and the envd HTTP API 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 design tradeoff a human maintainer should explicitly sign off on, even though the bug-hunting pass and my own reading didn't surface a defect.

Other factors

Test coverage is thorough — pool-identity tests per stack, HTTP-version keying, proxy-credential isolation, and new frame-level tests specifically pinning the shared-connection/redial behavior for the health-probe path (handle_rpc_exception_with_health). The PR also separately documents (and files as SDK-332, not fixed here) a pre-existing RAM-buffering issue in the retry middleware for streamed uploads — worth a maintainer's awareness but out of scope for this change. Given the combination of scope, shared mutable state, and an acknowledged behavioral change, I'm deferring rather than approving.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SDK test coverage report

Measured base 02ba746e (the true merge-base) against head 7f5cd88c, with E2B_API_KEY set so the live suites actually ran in both regimes.

Every changed source file sits at 100% statements and 100% branches on both sides, all 62 added executable lines are covered, no file anywhere in the package lost line or branch coverage, and all 14 new tests ran and passed. The package total dips 0.08pp purely because the refactor deletes 42 well-covered statements — the numerator falls faster than the denominator when you remove covered code.

python-sdk

base 02ba746e head 7f5cd88c
statements 69.30% (10201/14720) 69.22% (10161/14678)
branches 46.10% (1299/2818) 46.00% (1293/2810)
hand-written statements 83.20% (5246/6305) 83.13% (5206/6263)
generated statements 58.9% (4955/8415) 58.9% (4955/8415)
tests 899 passed / 2 failed / 58 skipped 907 passed / 2 failed / 58 skipped

Diff coverage of added source lines: 62/62 (100%), nothing uncovered.

changed file statements base → head branches
e2b/api/client_sync/__init__.py 38/38 → 41/41 (100%) 4/4 → 4/4
e2b/api/client_async/__init__.py 38/38 → 41/41 (100%) 4/4 → 4/4
e2b/envd/client_sync/__init__.py 39/39 → 29/29 (100%) 8/8 → 6/6
e2b/envd/client_async/__init__.py 57/57 → 47/47 (100%) 12/12 → 10/10
e2b/envd/client_shared.py 42/42 → 42/42 (100%) 8/8 → 8/8
e2b/volume/client_sync/__init__.py 36/36 → 22/22 (100%) 4/4 → 2/2
e2b/volume/client_async/__init__.py 36/36 → 22/22 (100%) 4/4 → 2/2
e2b/sandbox_sync/filesystem/filesystem.py 163/192 (84.9%), unchanged 47/68, unchanged
e2b/sandbox_async/filesystem/filesystem.py 178/214 (83.2%), unchanged 49/72, unchanged

The two filesystem.py files only have docstring edits (get_envd_transportget_transport), so their unchanged numbers are expected.

Unaffected packages, for context

This is a python-only change, and the other two packages measured identical to the same base commit, confirming no cross-package effect: js-sdk 81.96% lines (2159/2634), 82.05% statements, 72.72% branches, 88.82% functions, 611 passed / 1 failed / 33 skipped; cli 17.05% lines (236/1384), 21.72% branches, 109 passed. The cli figure is the usual artifact of its tests invoking the built CLI as a subprocess, which v8 cannot instrument — not untested code.

The three failures (test_firewall_transform_injects_headers in both python flavors, and the js-sdk httpbin sidecar test) are the pre-existing environmental ones: the test org has no httpbin template. All three reproduce identically on base.

Executed vs. asserted

Coverage percentages are the weak part of any report on a refactor like this, so the more useful question is which of the changeset's claims a test would actually catch a revert of. All four unification claims have an assertion behind them:

  • REST and envd HTTP share a pooltest_{sync,async}_envd_and_api_share_one_transport, plus envd_negotiated is negotiated in the http-version tests.
  • envd RPC runs on the shared pooltest_rpc_clients_run_on_the_shared_pool records what PlainHTTPErrorTransport is handed and asserts it is the cached pool, working around pyqwest.SyncClient not exposing its transport.
  • The volume content API runs on the shared pooltest_volume_transports_are_the_shared_sdk_pools, streaming variant included.
  • The actual payoff: one HTTP/2 connection per sandboxtest_shared_transport_pool.py counts accepted TCP connections against a real h2 server: one after RST_STREAM, two after a TCP drop. This is the strongest test in the PR, because it fails if reqwest stops reusing the connection, not merely if an object identity changes.
  • The new hazard the PR creates — one close() reaching a now process-wide pool — is pinned by test_{sync,async}_closing_one_client_leaves_the_shared_pool_open, which drives real traffic through both the sibling httpx client and the raw pool after closing one client.

The six tests that disappear are all replaced rather than dropped: two renames (get_transportget_pyqwest_transport in the envd modules), *_envd_transports_keyed_by_streaming folded into *_envd_and_api_share_one_transport, and test_transport_stack_normalizes_plain_errors_and_retries_connects split — its retry half into test_shared_pool_retries_connects, its normalization half already covered independently by the untouched test_envd_plain_http_errors.py.

One incidental strength worth keeping: because e2b/connection_config.py and e2b/volume/connection_config.py each define their own READ_TIMEOUT = 60.0, the new volume identity assertion only passes while the two constants agree, so it now pins them in sync.

Gaps

Two seams left, detailed inline. Neither is a regression this PR introduces, but the first one's blast radius grows now that a single pool serves every stack: nothing asserts that pool_max_idle_per_host, pool_idle_timeout, tls_include_system_certs or follow_redirects=False reach the pyqwest constructor, and test_shared_transport_pool.py proves connection reuse on a hand-built pool rather than the SDK's own, so pool wiring and pool reuse are verified by different tests with nothing joining them end to end.

A lock-ordering regression is covered by accident but reliably: _transport_lock is a non-reentrant threading.Lock, so moving the get_pyqwest_transport call inside it would deadlock, and pytest.ini sets timeout = 30, which turns that into a failure rather than a hung job. Note also that no workflow passes coverage flags and there are no thresholds anywhere in CI, so none of these numbers gate the merge either way.

Open in Web View Automation 

Sent by Cursor Automation: /coverage SDK Test Coverage Report

tls_include_system_certs=True,
proxy=proxy.to_pyqwest() if proxy is not None else None,
pool_idle_timeout=pool_idle_timeout,
pool_max_idle_per_host=pool_max_idle_per_host,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coverage gap worth closing while this code is fresh: no test asserts that these pool-tuning kwargs actually reach the pyqwest constructor. test_env_var_parsing.py pins the e2b.api module constants, and test_sync_transports_pass_http_version_to_pyqwest pins http_version — but it captures only kwargs["http_version"] out of a record(**kwargs) hook that already has the whole dict in hand.

That gap predates this PR, but its blast radius grows here. Setting pool_max_idle_per_host=0 (or dropping tls_include_system_certs, or flipping follow_redirects) would leave every identity assertion in test_api_client_transport.py green and every frame-level test in test_shared_transport_pool.py green, while a sandbox redialed on every request — precisely the regression this PR exists to prevent. Since the monkeypatch hook is already there, asserting the rest of the recorded kwargs is a couple of lines.


def _sync_pool() -> SyncConnectionRetryTransport:
return SyncConnectionRetryTransport(
SyncHTTPTransport(http_version=HTTPVersion.HTTP2),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pool is hand-built rather than obtained from get_pyqwest_transport, and the module docstring gives the good reason (the SDK's transports negotiate the version over TLS via ALPN, and this server is plaintext, so the test needs HTTP/2 prior knowledge).

The consequence for coverage is worth naming: connection reuse is proven here on a pool that omits the SDK's actual tuning, while pool wiring is proven by the identity assertions elsewhere, and nothing joins the two end to end. Combined with the untested constructor kwargs, that leaves one narrow seam where a tuning change could keep both halves green and still cost a connection per request. A cheap way to narrow it would be building this pool through get_pyqwest_transport and overriding only http_version, so the rest of the tuning is the real thing.


try:
assert get_sync_transport(config) is api_client_sync.get_transport(api_config)
assert get_sync_streaming_transport(config) is api_client_sync.get_transport(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice side effect: e2b/connection_config.py and e2b/volume/connection_config.py each declare their own READ_TIMEOUT = 60.0, so this identity assertion holds only while the two agree — changing one alone now fails here rather than silently splitting the streaming pool in two.

That also happens to be the only thing pinning the streaming pool's idle bound at the wiring level. The value itself is never asserted; streaming_transport is not api_transport in test_api_client_transport.py rules out None, and the stall/slow-transfer semantics are covered further down this file with READ_TIMEOUT monkeypatched to 0.3s. That combination is reasonable — just noting there is no sandbox-filesystem equivalent of those stall tests, only the _envd_api_streaming wiring assertion in test_sync_client_lifecycle.py.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taste review: pool unification

First, a caveat on scope: TASTE.md was not reachable from this run. https://raw.cursorusercontent.com/e2b/sdk-harness/main/TASTE.md returns 404, neither e2b/sdk-harness nor e2b-dev/sdk-harness resolves with this run's read-only token, and no copy exists in this repo or on the VM. So this is not a line-by-line check against that file — it is a review against the distilled rules from previous runs of this automation, plus verification of the PR's own claims.

On the taste rules there is genuinely little to bite on: this is internal transport plumbing with no user-facing surface change, and the new module-level names (get_pyqwest_transport, get_httpx_transport, TransportKey) match the existing precedent in the same modules (get_transport, get_envd_transport, the retrying_http_transport they replace). Sync and async mirror each other faithfully, comment-for-comment, with only the intended runtime/thread wording differing. So I spent the run on the substance instead, and found one real problem plus two inaccurate rationales.

The new test is flaky, and I can prove why

test_sync_dropped_connection_redials_for_the_health_probe failed on my first full run of tests/*.py (263 passed, 1 failed), then passed roughly a hundred subsequent executions — plain, --numprocesses=4 like CI, cold bytecode, pinned to a starved single CPU. There is no random ordering plugin here, so it is a timing race rather than an order dependency. The failure:

ConnectError: Request failed: error sending request for url
(http://127.0.0.1:45755/process.Process/Connect):
client error (SendRequest): connection error: connection reset

That is the first next(events) raising, so the exception escaped _break_sync_stream's pytest.raises block rather than being the failure the test wants. The cause is in SharedPoolServer._serve: in "drop" mode it sendalls the response head plus the first event and then RSTs the connection immediately, with nothing synchronizing against the client having consumed it. I confirmed the mechanism by mutation — moving the SO_LINGER/return above the sendall makes both drop tests fail with a byte-identical error message. (The naive TCP explanation is not it: a standalone socket probe showed Linux hands buffered data to recv before reporting ECONNRESET. The race resolves inside hyper, whose connection task fails the in-flight request with the connection error instead of dispatching the response head it already has.)

Details and a verified fix are in the inline comment on tests/envd_frame_server.py.

Two rationales that do not match reality

Neither changes behavior, but both are the kind of claim that outlives the diff and gets trusted later:

  • get_envd_transport's reason for existing is wrong. Its new docstring says e2b-code-interpreter reaches into it; that package actually calls e2b.api.client_{sync,async}.get_transport(config, http2=False), and get_envd_transport appears nowhere in its 119 Python files. After this PR nothing inside e2b/ calls it either, so it is dead surface kept on a false premise.
  • tests/transport_caches.py lists template uploads among the stacks drawing from the shared pool. They build their own transport inline in template_{sync,async}/build_api.py — as this PR's own description correctly says — so reset_transport_caches() does not in fact "clear them all."

What I verified as accurate

Every number in the description reproduced, and the substantive claims hold up:

Claim Result
uv run pytest tests/*.py -q → 264 passed Reproduced (after the flake above)
uv run pytest tests/shared -q → 128 passed, 1 skipped Reproduced exactly
format / lint / typecheck clean Reproduced: ruff check clean, 409 files already formatted, ty check clean
Adapters never close the wrapped pool Confirmed in the pinned pyqwest: neither PyqwestTransport nor AsyncPyqwestTransport defines close/aclose, so both inherit httpx's no-ops
The new close tests are not vacuous Confirmed by mutation: forwarding close/aclose through to the pool makes both fail with exactly RuntimeError: Executing request on already closed transport. Worth knowing that the adapter holds ConnectionRetryTransport, which exposes no close at all — so the pool sits one layer further from an accidental close than the description implies
Sharing one retry middleware across the RPC and httpx stacks is safe SyncRetryTransport/RetryTransport hold only immutable backoff configuration — no per-request or cross-request state

One thing the description does not mention that I checked because it looked like a silent behavior change: routing the volume transports through get_httpx_transport now passes http_version=None where they previously passed nothing. pyqwest's default for that argument is None, so the negotiated version is unchanged.

Finally, a stale line in the description: it asks that #1659 be closed in favour of this PR, but #1659 is already closed. (I have not acted on that request either way — read-only token, and closing PRs is not this automation's job.)

The refactor itself looks right to me and the disclosed HTTP/2 stream-budget change is the correct thing to flag for a human. The flaky test is the one item I would fix before merge.

Open in Web View Automation 

Sent by Cursor Automation: /review SDK complies with TASTE.md

Comment thread packages/python-sdk/tests/envd_frame_server.py
Comment thread packages/python-sdk/e2b/api/client_sync/__init__.py Outdated
Comment thread packages/python-sdk/tests/transport_caches.py Outdated
Comment thread packages/python-sdk/e2b/volume/client_sync/__init__.py
@mishushakov

Copy link
Copy Markdown
Member

@cursor check

@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

I addressed the concrete, well-supported issues the review bots raised on this PR. All changes are Python-SDK-only; ruff format/ruff check/ty are clean and tests/*.py is green (266 passed), with the previously-flaky drop tests passing 15/15 runs.

What I changed (each its own commit, pushed to cursor/pr-claiming-mechanism-1180):

  • Removed the race in the shared-pool "drop" tests (envd_frame_server.py, test_shared_transport_pool.py): SharedPoolServer RST'd the TCP connection right after writing the response head + first event, so an RST that won the race made hyper fail the in-flight request instead of yielding the event — the failure both bots flagged. The teardown now waits on a threading.Event the test sets only after consuming that event, so the RST can't race the response head. Redial coverage (connections == 2) is unchanged.
  • Corrected inaccurate docs: get_envd_transport's rationale (verified via GitHub search that e2b-code-interpreter imports get_transport, not get_envd_transport; kept it as an honestly-labeled backward-compat alias) and the transport_caches.py note (template uploads build their own inline transport and aren't cached).
  • Closed the coverage seam the coverage bot named: new sync/async tests assert pool_max_idle_per_host, pool_idle_timeout, tls_include_system_certs, read_timeout, and follow_redirects=False actually reach the pyqwest constructor — a tuning regression that would otherwise keep every identity test green while a sandbox redialed per request.
  • Documented the READ_TIMEOUT coupling next to both constants so a lone edit doesn't silently split the shared streaming pool.

I left the PR title/description as-is since the change wasn't requested there and the commit messages are self-describing. Note the acknowledged HTTP/2 stream-budget behavior change under heavy per-sandbox concurrency still warrants a human sign-off before merge.

Open in Web Open in Cursor 

cursoragent and others added 5 commits August 19, 2026 14:58
The SharedPoolServer RST'd the TCP connection immediately after writing the
response head and first event, with nothing ensuring the client had consumed
them. When the RST won, hyper failed the in-flight request with the connection
error instead of yielding the first event, so next(events) raised inside the
stream helper's assert rather than its pytest.raises block, failing the two
'drop' tests intermittently.

Gate the connection teardown on a threading.Event the test sets only after it
has read the event written before the fault, so the RST can no longer race the
response head. Harmless for the 'reset' tests, which never wait on it.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
The docstring claimed e2b-code-interpreter reaches into get_envd_transport,
but that package imports get_transport directly and calls no such name in any
of its files; nothing inside the SDK calls get_envd_transport either. Keep it
as a backward-compatible alias of get_transport for any external importer of
the older public name, and describe it as exactly that.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
reset_transport_caches() only clears the shared REST/envd/volume pools;
template uploads build their own non-retrying transport inline and are not
cached here, so listing them overstated what the helper resets.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
…nfigs

The volume streaming pool and the sandbox-filesystem streaming pool are the
same reqwest pool only while e2b.connection_config.READ_TIMEOUT and
e2b.volume.connection_config.READ_TIMEOUT agree, since the read bound is part
of the transport cache key. Flag the coupling next to both constants so a lone
change does not silently split the pool in two.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
The identity assertions prove one pool is reused but not how it was built, so
dropping pool_max_idle_per_host, the system CA certs, or follow_redirects=False
would leave every identity and frame-level test green while a sandbox redialed
on every request or TLS broke through an intercepting proxy. Record the full
kwargs handed to the pyqwest transport and assert the tuning, using the
streaming pool so read_timeout is pinned too.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
@mishushakov

Copy link
Copy Markdown
Member

/sdk review

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taste review: pool unification

TASTE.md came through in full this run — earlier runs on this PR had to fall back on a distilled copy — so this is a genuine pass against the actual document. All four findings from my last review are fixed, three of them exactly as suggested, and I re-ran everything rather than reading the commits.

Verified

check result
uv run pytest tests/*.py -q 266 passed (the body still says 264)
uv run pytest tests/shared -q 128 passed, 1 skipped — as claimed
make lint / make typecheck / make format clean, 409 files left unchanged — as claimed
tests/test_shared_transport_pool.py × 30 30/30 green
  • The drop-test race fix is structural, not a timing tweak. Gating the teardown on drop_when means the RST can no longer precede the client's read of the first event, which is exactly the mechanism I demonstrated last time by hoisting the SO_LINGER above the sendall. The 5-second bound on the wait is a good touch — a test that forgets to set the event degrades to the old behaviour instead of hanging into pytest's 30s timeout.
  • "No user-facing API change" holds. e2b/__init__.py is untouched, so nothing enters or leaves __all__, and TASTE's flat-entry-point rule is unaffected.
  • sync/async parity is clean. I normalised away the idiom (async def, await, the Async*/Sync* class names, client_syncclient_async) and diffed all three mirror pairs. The only differences left are the ones that should be there: make_async_logging_event_hooks, and "thread-safe" vs "loop-independent" in the docstrings. Given this change rewrites six parallel modules, semantic drift between the mirrors was the failure I most expected to find, and it isn't there.
  • The volume stack genuinely is unconfigured-identical. It previously built its transport without passing http_version at all, and now reaches the shared pool via http2=Truehttp_version=None; the pinned pyqwest stub has http_version: HTTPVersion | None = None as the constructor default, so the two are the same configuration rather than merely similar.
  • Both outright deletions are safe. gh search code turns up no user of retrying_http_transport anywhere outside this repo (the only other hit is a package-metadata scraper), and nothing imports e2b.envd.client_sync at all. So a patch changeset is defensible on impact — see the second inline comment for why the policy is still worth writing down.

Findings

Three, none of them blocking:

  1. A wrong version number in the changeset, which lands verbatim in the published CHANGELOG: the http2 restore shipped in @e2b/python-sdk 2.39.1, not 2.38.1 (the Python SDK has no 2.38.1 at all).
  2. get_envd_transport is now a compatibility alias in everything but its docstring field. TASTE asks Python deprecations to carry :deprecated: with a migration path and a removal horizon; the corrected rationale in this PR is what makes the rule clearly apply.
  3. A one-character naming nit on TransportKey.

The docstrings throughout this change are unusually good — get_pyqwaseget_pyqwest_transport explaining why read_timeout and http2 have to be cache-key components, and the new coupling notes beside both READ_TIMEOUT constants, are the kind of thing TASTE means by treating docstrings as part of the API.

Open in Web View Automation 

Sent by Cursor Automation: /review SDK complies with TASTE.md

carrying the idle `read_timeout`: reqwest's read timer runs during body send and
TTFB, so on a shared pool it would cut off long uploads. No signature changes —
`get_transport` and `get_envd_transport` keep the `http2` parameter restored in
2.38.1, and the two are now the same pool per key rather than two.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The version here is wrong: the http2 parameter came back in @e2b/python-sdk 2.39.1, not 2.38.1. The Python SDK has no 2.38.1 release at all — packages/python-sdk/CHANGELOG.md runs 2.38.0 → 2.39.0 → 2.39.1 → 2.40.0, and #1671's entry sits under the ## 2.39.1 heading. 2.38.1 exists only as an e2b (JS SDK) tag, which is probably where it came from.

Worth correcting because changeset prose is copied verbatim into the published CHANGELOG, and this is precisely the sentence a reader consults to find out which release to pin to keep http2 working.

 `get_transport` and `get_envd_transport` keep the `http2` parameter restored in
-2.38.1, and the two are now the same pool per key rather than two.
+2.39.1, and the two are now the same pool per key rather than two.

now share one pool per key, since reqwest pools per host and envd RPC and
the envd HTTP API hit the same sandbox host anyway.

Kept only as a backward-compatible alias of :func:`get_transport` for any

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The corrected rationale is accurate now — and it also settles this function's status: an alias kept solely for external importers of an older name, called nowhere inside the SDK. That is TASTE's definition of a deprecated identifier, so the deprecation rule applies directly: mark it with the Python :deprecated: field including the migration path and, where applicable, the removal horizon. At the moment the migration path exists only as prose ("Prefer get_transport"), so it won't render as deprecated in the docs and nothing dates it.

What makes this more than bookkeeping is that the same PR deletes two equally public-named neighbours outright — retrying_http_transport in this module and get_transport in e2b.envd.client_{sync,async} — with no alias and no note. So this module's compatibility policy is currently decided per name, with nothing recording the reasoning. I checked and both deletions are safe in practice (no external importer of either), so the ask is only to write the policy down where the next refactor will see it. The cautionary precedent is six days old: #1671 shipped in 2.39.1 to restore http2 on these very factories because e2b-code-interpreter imports them, even though #1671's own description reasoned they were internal.

     Kept only as a backward-compatible alias of :func:`get_transport` for any
     external importer of the older public name; nothing inside the SDK calls it
     (``e2b-code-interpreter`` imports :func:`get_transport` directly). Prefer
     :func:`get_transport`.
+
+    :deprecated: Use :func:`get_transport` instead; will be removed in the next
+        major version.
     """

The async mirror in e2b/api/client_async/__init__.py needs the same line.



def retrying_http_transport(
TransportKey = Tuple[Optional[ProxyConfig], Optional[float], bool]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nit, and only a naming one: TransportKey is the key type for _transports and _httpx_transports and is used nowhere else, yet it is the one name in this group without the underscore its neighbours all carry (_transport_lock, _transports, _httpx_transports immediately below). TASTE keeps values the SDK needs but users don't underscore-prefixed, so _TransportKey would match both the rule and the surrounding lines. The docstring explaining why all three components are fixed at construction time is worth keeping either way — that's the part a future reader needs.

Same in the async mirror.

@mishushakov

Copy link
Copy Markdown
Member

@cursor check

@cursor

cursor Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the three non-blocking taste-review findings; all pushed to cursor/pr-claiming-mechanism-1180.

  • Changeset version: 2.38.12.39.1 (the http2 restore actually shipped in 2.39.1; Python has no 2.38.1).
  • get_envd_transport: marked :deprecated: with migration path and next-major removal horizon (sync + async), matching TASTE and the beta_pause precedent.
  • TransportKey_TransportKey: matches the underscore-prefixed neighbours in the same module.

ruff/ty clean; transport tests 39/39 green. Couldn’t update the PR body (not agent-managed) — the description still says 264 passed and still claims e2b-code-interpreter calls get_envd_transport; worth a manual edit to match the commits.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 19, 2026 15:33
The http2 parameter came back in @e2b/python-sdk 2.39.1 (#1671), not 2.38.1 —
the Python SDK has no 2.38.1 release. Changeset prose lands verbatim in the
published CHANGELOG, so the wrong pin would mislead anyone looking for which
release keeps http2 working.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
…nsportKey

get_envd_transport is now an alias kept solely for external importers of the
older name and called nowhere inside the SDK — mark it :deprecated: with the
migration path and a next-major removal horizon, matching TASTE and the
existing beta_pause precedent.

Rename TransportKey to _TransportKey so the cache-key type matches its
underscore-prefixed neighbours (_transport_lock, _transports,
_httpx_transports); it is module-private and used nowhere else.

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
@mishushakov
mishushakov enabled auto-merge (squash) August 19, 2026 16:35
@mishushakov
mishushakov merged commit 666241d into main Aug 19, 2026
22 checks passed
@mishushakov
mishushakov deleted the cursor/pr-claiming-mechanism-1180 branch August 19, 2026 16:35
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.

2 participants