fix(python-sdk): return from _iterate_events after end event to unblock wait()#1299
fix(python-sdk): return from _iterate_events after end event to unblock wait()#1299FisherXZ wants to merge 5 commits intoe2b-dev:mainfrom
Conversation
…ck wait() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…2b-dev#1034 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: a6bb6ad 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbbad45b2c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| exit_code=event.event.end.exit_code, | ||
| error=event.event.end.error, | ||
| ) | ||
| return |
There was a problem hiding this comment.
Close event stream before returning on end event
Returning immediately here stops consuming self._events, but the stream generator is kept on the handle and is not closed in wait() (and disconnect() does not aclose() it). The async stream implementation (e2b_connect.Client.acall_server_stream) holds the HTTP stream inside an async with, so exiting early without closing/exhausting the generator can keep connections open until the handle is garbage-collected; in workloads that keep many handles, this can accumulate and exhaust the async connection pool.
Useful? React with 👍 / 👎.
…k to release HTTP connection Without this, returning early from _iterate_events after the "end" event leaves self._events (acall_server_stream) suspended with its async with http_resp block still open. The TCP connection is held until the handle is GC'd. Adding aclose() in finally releases it deterministically in all paths: normal completion, exception, and cancellation from disconnect(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue
Fixes #1034.
AsyncCommandHandle.wait()blocks indefinitely afterkill().Investigation
Traced the hang through the Python async SDK:
_iterate_eventsreceives the"end"event from envd, setsself._result— then continues theasync forloop waiting for the next event. If envd is slow to close the HTTP stream after sending"end", the loop waits indefinitely.wait()blocks with it.A secondary issue was also found during investigation: even after fixing the blocking,
self._events(theacall_server_streamasync generator) held itsasync with http_resp:block open until the handle was garbage-collected. Because HTTP/1.1 is used (not HTTP/2), each unclosed handle tied up one TCP connection. In workloads that accumulate handles, this can exhaust the connection pool (max_connections=2000).Fix
Commit 1 — stop blocking: Add
returnafter settingself._resultin_iterate_events. The generator exits immediately on"end", so_handle_eventsterminates andwait()unblocks regardless of stream-close timing.Commit 2 — release connection: Add
await self._events.aclose()in afinallyblock in_handle_events. This releases the HTTP connection deterministically in all three paths:async forends;finallycallsaclose()finallycallsaclose()disconnect()→ task cancelledCancelledErrorbreaks loop;finallycallsaclose()Calling
aclose()in thefinallyof the coroutine that owns the iteration avoids the concurrent-accessRuntimeErrorthat's why the# await self._events.aclose()line indisconnect()was commented out.Two lines changed total. No new state, no new exception types, no behavioural change to
kill()ordisconnect().This also applies to PTY handles —
AsyncCommandHandleis shared bypty.py, so PTY sessions now terminate promptly and release their connection after the process exits.Tests
test_kill_via_handle— verifieshandle.kill()(the instance method) returnsTrueand the process is dead. The existingtest_kill_processusessandbox.commands.kill(pid)— a different code path.test_kill_handle_wait_raises— verifieshandle.wait()raisesCommandExitExceptionwithin 5 seconds afterhandle.kill().asyncio.wait_for(..., timeout=5.0)is a safety net: if the fix regresses, the test fails withTimeoutErrorinstead of hanging silently.Out of scope
__aiter__onAsyncCommandHandle— PR fix: add __aiter__ to AsyncCommandHandle for async iteration support #1192 creates a second consumer ofself._events→RuntimeError. Separate issue.AsyncWatchHandlehas the identical# await self._events.aclose()pattern. Same fix applies but left for a separate PR to keep scope small.AbortControlleralready handles clean stream cancellation. Unaffected.🤖 Generated with Claude Code