Load-balance envd HTTP/2 pools by in-flight streams; keep HTTP version a transport-only argument - #1889
Load-balance envd HTTP/2 pools by in-flight streams; keep HTTP version a transport-only argument#1889devin-ai-integration[bot] wants to merge 4 commits into
Conversation
….1 option Python: replace the fixed CRC32(sandbox_id) shard with EnvdPoolBalancer, a least-loaded picker that opens a further pool only once every open pool carries E2B_ENVD_POOL_STREAMS (90) requests, bounded by E2B_ENVD_POOL_SHARDS (now 16). Envd RPC and HTTP share the balancer; responses are re-wrapped so a request is counted until its body is read or closed. Python + JS: sandbox_http2 / sandboxHttp2 connection option and E2B_SANDBOX_HTTP2 env var pin envd traffic to HTTP/1.1. Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
🦋 Changeset detectedLatest commit: 9ae4d13 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Package ArtifactsBuilt from dda57e2. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.51.1-devin-1789734207-envd-pool-growth-http1.0.tgzCLI ( npm install ./e2b-cli-2.20.1-devin-1789734207-envd-pool-growth-http1.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.8.1-devin-1789734207-envd-pool-growth-http1.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.4.1-devin-1789734207-envd-pool-growth-http1.0.tgzPython SDK ( pip install ./e2b-2.51.0+devin.1789734207.envd.pool.growth.http1-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.10.0+devin.1789734207.envd.pool.growth.http1-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.6.0+devin.1789734207.envd.pool.growth.http1-py3-none-any.whl |
There was a problem hiding this comment.
TASTE.md review (sdk-harness) of the changed surface only.
Checked: parity (T-1, T-2, T-10), API shape (T-3/T-3a, T-6, T-12, T-14, T-22/T-23), configuration (T-47, T-49–T-51), docs (T-69–T-71).
1 violation, flagged inline on both SDKs (same issue):
- T-14 — the new public option
sandboxHttp2/sandbox_http2is a boolean selecting a transport behavior (HTTP/2 vs HTTP/1.1). TASTE prefers a string-literal union for behavior-selecting options so a third variant (e.g.'auto', HTTP/3) is not a breaking change; the negated spelling (falsemeans "use HTTP/1.1") also reads poorly at the call site. Same applies to theE2B_SANDBOX_HTTP2env var, which would becomeE2B_SANDBOX_HTTP_VERSION(T-49).
Compliant elsewhere: option name mirrors across SDKs (T-10) and extends ConnectionOpts / ApiParams; precedence explicit → E2B_ env var → default via ConnectionConfig (T-49), and get_api_params() forwards it so static/instance reconnects keep it (T-6); flag-shaped env parsing with a sentinel is fine per T-50; JSDoc has @default, Python docstring documents the default (T-69/T-70). The Python-only balancer is transport plumbing with no public surface, so no parity concern (T-1).
Not tied to a diff line worth an inline comment: the new internal helpers createEnvdFetch(proxy?, http2 = true) and get_envd_pyqwest_transport(proxy, read_timeout=None, http2=True) take chains of optional positionals (T-3 / T-3a). They follow the pre-existing shape of get_pyqwest_transport, and are not exported from the entry points, so this is a nit rather than a public-surface violation.
…imes out on port 9) Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
|
check the above |
|
I'd probably also remove sandbox_http2 setting and rely on global http2 setting instead (that we should already have) |
…nnect test deterministic Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Removed |
… option and E2B_HTTP_VERSION Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Summary
Fixes EN-2742 (from the 192-concurrent-trial Harbor/mini-swe-agent report: HTTP/2 stream starvation at
MAX_CONCURRENT_STREAMS=100). SDK-381 (a public HTTP/1.1 option) was considered and dropped per review: HTTP version stays a transport-layer detail, not a client/sandbox setting.Python: load-aware envd pools
The sandbox host allows 100 concurrent streams per HTTP/2 connection and a running command holds its stream for its whole lifetime. The SDK spread sandboxes over 4 fixed pools by
crc32(sandbox_id) % 4, so a hot sandbox (or an unlucky hash) still queued at 100, and a 1-sandbox process opened 4 connections anyway.envd_pool_shard(config)is replaced byEnvdPoolBalancerine2b.api:client_sync/client_asyncgainEnvdPoolTransport, a pyqwest transport over the balancer whose pools are the existing cachedget_pyqwest_transport(..., shard)transports (connect-retry included). It re-wraps the response so a request is counted in flight until its body is fully read, fails, or is closed — not just until the head arrives — via a_TrackedContentiterator that releases exactly once (pyqwest responses can't be subclassed, so it's composition over a freshResponse). Envd RPC (create_rpc_client) and envd HTTP (get_envd_transport,get_envd_api) draw on the sameget_envd_pyqwest_transport(proxy, read_timeout, http2)instance, so one balancer sees all streams of a process; control-plane traffic stays onget_transport/ shard 0. HTTP/1.1 gets a single pool (reqwest opens a connection per concurrent request there).Live check against real sandboxes: 120 background
sleep 30commands landed asactive_streams == (90, 30), a controlechowent through immediately, and counts dropped to 0 viawait(),disconnect(), and handle GC.Python + JS: HTTP version is a transport-only argument
No public option and no env var. The internal
http2: bool = Trueargument thatget_transportalready had now also exists onget_envd_transport/get_envd_pyqwest_transport(sync + async), and on the JS fetch factories, so a transport can be built for HTTP/1.1 without touchingConnectionConfig/Sandbox:No JS balancer: undici already opens connections on demand (
E2B_ENVD_RPC_CONNECTIONS, default 200) — verified locally admitting 192 held HTTP/2 streams + a control request.Not in this PR
kill_reasonexposure onSandboxNotFoundException/get_info()(needs the belt OpenAPI schema first), and the source of themax_ageGOAWAY (not emitted by envd/runtime/Terraform; an intermediary).Tests
test_envd_stream_capacity.py(192 streams over a real HTTP/2 server withMaxConcurrentStreams=100→(90, 90, 12), releases on close, no starvation for many- and one-sandbox cases),test_api_client_transport.py(balancer growth/caps/thread churn, body-lifetime accounting incl. a refused connect via a stubbed pool — the earlier closed-port version timed out on Windows — envd vs control-plane cache identity, HTTP-version forwarding),test_envd_client_transport.py(RPC + HTTP share the envd transport),test_env_var_parsing.py, JSapi/http2andenvd/http2tests.pnpm run format/lint/typecheckclean; Python suite 1213 passed (2httpbin-template failures are pre-existing/account-specific); JS unit suite 163 passed; live sync+async commands/files/pty integration tests pass on HTTP/2 and on HTTP/1.1 transports.Link to Devin session: https://app.devin.ai/sessions/57b07cfa1dc9453eabf11f0617365534
Open in Devin Desktop: https://app.devin.ai/desktop/session/57b07cfa1dc9453eabf11f0617365534?variant=devin
Requested by: @mishushakov