Skip to content

Commit 246ba1e

Browse files
event loop is blocked - fixing
1 parent 6938d37 commit 246ba1e

File tree

2 files changed

+32
-28
lines changed

2 files changed

+32
-28
lines changed

stagehand/main.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -454,38 +454,39 @@ async def init(self):
454454

455455
async def _init_playwright_in_thread(self):
456456
"""
457-
Initialize playwright in a separate thread for compatibility with strict event loops.
457+
Initialize playwright using asyncio.to_thread to avoid blocking the main event loop.
458458
459-
This method runs the playwright initialization in a separate thread to avoid
460-
blocking operations that can conflict with strict event loop environments
461-
like Langgraph when running without --allow-blocking.
459+
This method runs the potentially blocking async_playwright().start() in a thread
460+
to avoid conflicts with strict event loop environments like Langgraph.
462461
"""
463-
def _start_playwright():
464-
"""Helper function to start playwright in a separate thread."""
462+
self.logger.debug("Starting playwright initialization in background thread...")
463+
464+
def _start_playwright_blocking():
465+
"""Start playwright in a blocking manner - will be run in a thread."""
465466
import asyncio
467+
from playwright.async_api import async_playwright
466468

467469
# Create a new event loop for this thread
468470
loop = asyncio.new_event_loop()
469471
asyncio.set_event_loop(loop)
470472

471473
try:
472-
# Start playwright in this thread's event loop
474+
# Run the async playwright start in this loop
473475
return loop.run_until_complete(async_playwright().start())
474476
finally:
475-
# Clean up the loop
476477
loop.close()
477478

478-
self.logger.debug("Starting playwright in separate thread...")
479-
480479
try:
481-
# Run the playwright initialization in a thread
482-
playwright_instance = await asyncio.to_thread(_start_playwright)
483-
self.logger.debug("Playwright initialized successfully in thread")
480+
# Use asyncio.to_thread to run the blocking initialization
481+
playwright_instance = await asyncio.to_thread(_start_playwright_blocking)
482+
483+
self.logger.debug("Playwright initialized successfully in background thread")
484484
return playwright_instance
485+
485486
except Exception as e:
486-
self.logger.error(f"Failed to initialize playwright in thread: {e}")
487+
self.logger.error(f"Failed to initialize playwright in background thread: {e}")
487488
raise RuntimeError(
488-
"Failed to initialize Playwright in thread. This may indicate a "
489+
"Failed to initialize Playwright in background thread. This may indicate a "
489490
"deeper compatibility issue with your environment."
490491
) from e
491492

tests/unit/test_client_initialization.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,18 @@ async def mock_create_session():
205205
await client._create_session()
206206

207207
@pytest.mark.asyncio
208-
@mock.patch("stagehand.main.async_playwright")
209-
async def test_init_playwright_in_thread(self, mock_async_playwright):
210-
"""Test that playwright initialization works properly in a separate thread."""
208+
@mock.patch("asyncio.to_thread")
209+
async def test_init_playwright_in_thread(self, mock_to_thread):
210+
"""Test that playwright initialization works properly using asyncio.to_thread."""
211211
# Create a mock playwright instance
212212
mock_playwright_instance = mock.AsyncMock()
213213
mock_playwright_instance.stop = mock.AsyncMock()
214214
mock_playwright_instance.chromium = mock.MagicMock()
215+
mock_playwright_instance.firefox = mock.MagicMock()
216+
mock_playwright_instance.webkit = mock.MagicMock()
215217

216-
# Mock the async_playwright().start() to return our mock instance
217-
mock_async_playwright_start = mock.AsyncMock(return_value=mock_playwright_instance)
218-
mock_async_playwright.return_value.start = mock_async_playwright_start
218+
# Mock asyncio.to_thread to return our mock instance
219+
mock_to_thread.return_value = mock_playwright_instance
219220

220221
# Create a Stagehand client with LOCAL env
221222
config = StagehandConfig(env="LOCAL")
@@ -227,26 +228,28 @@ async def test_init_playwright_in_thread(self, mock_async_playwright):
227228
# Verify that the playwright instance was returned
228229
assert result is mock_playwright_instance
229230

230-
# Verify that async_playwright().start() was called
231-
mock_async_playwright_start.assert_called_once()
231+
# Verify that asyncio.to_thread was called
232+
mock_to_thread.assert_called_once()
232233

233234
# Verify the result has the expected attributes
234235
assert hasattr(result, 'chromium')
236+
assert hasattr(result, 'firefox')
237+
assert hasattr(result, 'webkit')
235238
assert hasattr(result, 'stop')
236239

237240
@pytest.mark.asyncio
238-
@mock.patch("stagehand.main.async_playwright")
239-
async def test_init_playwright_in_thread_handles_exceptions(self, mock_async_playwright):
241+
@mock.patch("asyncio.to_thread")
242+
async def test_init_playwright_in_thread_handles_exceptions(self, mock_to_thread):
240243
"""Test that threaded playwright initialization properly handles exceptions."""
241-
# Mock async_playwright().start() to raise an exception
242-
mock_async_playwright.return_value.start.side_effect = Exception("Test exception")
244+
# Mock asyncio.to_thread to raise an exception
245+
mock_to_thread.side_effect = Exception("Test exception")
243246

244247
# Create a Stagehand client with LOCAL env
245248
config = StagehandConfig(env="LOCAL")
246249
client = Stagehand(config=config)
247250

248251
# Test that the method raises a RuntimeError with our exception message
249-
with pytest.raises(RuntimeError, match="Failed to initialize Playwright in thread"):
252+
with pytest.raises(RuntimeError, match="Failed to initialize Playwright in background thread"):
250253
await client._init_playwright_in_thread()
251254

252255
@pytest.mark.asyncio

0 commit comments

Comments
 (0)