Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-async-command-handle-kill-blocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/python-sdk": patch
---

Fix `AsyncCommandHandle.wait()` blocking after `kill()`: `_iterate_events` now returns immediately after the "end" event, so `wait()` completes promptly even if the gRPC stream is slow to close.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ vale-styles
.vercel
.vscode
.DS_Store
.worktrees

# Logs
logs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async def _iterate_events(
exit_code=event.event.end.exit_code,
error=event.event.end.error,
)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.


async def disconnect(self) -> None:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

import pytest

from e2b import AsyncSandbox, CommandExitException
Expand All @@ -17,3 +19,20 @@ async def test_kill_non_existing_process(async_sandbox: AsyncSandbox):
non_existing_pid = 999999

assert not await async_sandbox.commands.kill(non_existing_pid)


async def test_kill_via_handle(async_sandbox: AsyncSandbox):
handle = await async_sandbox.commands.run("sleep 60", background=True)
killed = await handle.kill()
assert killed is True
with pytest.raises(CommandExitException):
await async_sandbox.commands.run(f"kill -0 {handle.pid}")


async def test_kill_handle_wait_raises(async_sandbox: AsyncSandbox):
handle = await async_sandbox.commands.run("sleep 60", background=True)
await handle.kill()
# Before the fix: this blocks forever (or until the 5s timeout fires as TimeoutError).
# After the fix: raises CommandExitException promptly because the process was killed.
with pytest.raises(CommandExitException):
await asyncio.wait_for(handle.wait(), timeout=5.0)