Skip to content

Commit ba62e3e

Browse files
revert to threading
1 parent d1400ff commit ba62e3e

File tree

1 file changed

+61
-34
lines changed

1 file changed

+61
-34
lines changed

stagehand/main.py

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -210,26 +210,47 @@ def cleanup_handler(sig, frame):
210210
# Try to get the current event loop
211211
try:
212212
loop = asyncio.get_running_loop()
213+
# Schedule cleanup in the existing event loop
214+
# Use call_soon_threadsafe since signal handlers run in a different thread context
215+
def schedule_cleanup():
216+
task = asyncio.create_task(self._async_cleanup())
217+
# Shield the task to prevent it from being cancelled
218+
asyncio.shield(task)
219+
# We don't need to await here since we're in call_soon_threadsafe
220+
221+
loop.call_soon_threadsafe(schedule_cleanup)
222+
213223
except RuntimeError:
214-
# No event loop running - create one to run cleanup
215-
print("No event loop running, creating one for cleanup...")
224+
# No event loop running - use thread-based cleanup to avoid blocking
225+
print("No event loop running, scheduling cleanup in separate thread...")
216226
try:
217-
asyncio.run(self._async_cleanup())
227+
# Use threading to avoid blocking operations in signal handler
228+
import threading
229+
230+
def cleanup_in_thread():
231+
try:
232+
# Create new event loop in thread for cleanup
233+
new_loop = asyncio.new_event_loop()
234+
asyncio.set_event_loop(new_loop)
235+
new_loop.run_until_complete(self._async_cleanup())
236+
except Exception as e:
237+
print(f"Error during threaded cleanup: {str(e)}")
238+
finally:
239+
try:
240+
new_loop.close()
241+
except Exception:
242+
pass
243+
sys.exit(0)
244+
245+
cleanup_thread = threading.Thread(target=cleanup_in_thread, daemon=True)
246+
cleanup_thread.start()
247+
# Give the cleanup thread time to start, but don't wait indefinitely
248+
cleanup_thread.join(timeout=5.0)
249+
sys.exit(0)
250+
218251
except Exception as e:
219252
print(f"Error during cleanup: {str(e)}")
220-
finally:
221-
sys.exit(0)
222-
return
223-
224-
# Schedule cleanup in the existing event loop
225-
# Use call_soon_threadsafe since signal handlers run in a different thread context
226-
def schedule_cleanup():
227-
task = asyncio.create_task(self._async_cleanup())
228-
# Shield the task to prevent it from being cancelled
229-
asyncio.shield(task)
230-
# We don't need to await here since we're in call_soon_threadsafe
231-
232-
loop.call_soon_threadsafe(schedule_cleanup)
253+
sys.exit(1)
233254

234255
except Exception as e:
235256
print(f"Error during signal cleanup: {str(e)}")
@@ -454,34 +475,40 @@ async def init(self):
454475

455476
async def _init_playwright_with_timeout(self):
456477
"""
457-
Initialize playwright with a timeout to avoid hanging in strict event loop environments.
478+
Initialize playwright in a separate thread to avoid blocking operations in strict event loop environments.
458479
459-
This method adds a timeout to the regular async_playwright().start() to prevent
460-
hanging in environments that restrict blocking operations.
480+
This method uses asyncio.to_thread to run Playwright initialization in a separate thread
481+
with its own event loop, preventing blocking operations from affecting the main event loop
482+
in environments like Langgraph that enforce strict async policies.
461483
"""
462-
self.logger.debug("Starting playwright initialization with timeout...")
484+
self.logger.debug("Starting playwright initialization in separate thread...")
463485

464486
try:
465-
# Use asyncio.wait_for to add a timeout to prevent hanging
466-
# If the environment doesn't allow blocking operations, this will fail fast
467-
playwright_instance = await asyncio.wait_for(
468-
async_playwright().start(), timeout=30.0 # 30 second timeout
469-
)
487+
# Run Playwright initialization in a separate thread to avoid blocking the main event loop
488+
# This is necessary for environments like Langgraph that have strict blocking operation restrictions
489+
def _init_playwright_in_thread():
490+
# Create a new event loop for this thread
491+
import asyncio
492+
new_loop = asyncio.new_event_loop()
493+
asyncio.set_event_loop(new_loop)
494+
495+
try:
496+
# Run the Playwright initialization in this thread's event loop
497+
return new_loop.run_until_complete(async_playwright().start())
498+
finally:
499+
new_loop.close()
470500

471-
self.logger.debug("Playwright initialized successfully")
501+
# Use asyncio.to_thread to run the blocking initialization in a separate thread
502+
playwright_instance = await asyncio.to_thread(_init_playwright_in_thread)
503+
504+
self.logger.debug("Playwright initialized successfully in separate thread")
472505
return playwright_instance
473506

474-
except asyncio.TimeoutError:
475-
self.logger.error("Playwright initialization timed out")
476-
raise RuntimeError(
477-
"Playwright initialization timed out after 30 seconds. This may indicate "
478-
"your environment has strict event loop restrictions."
479-
) from None
480507
except Exception as e:
481-
self.logger.error(f"Failed to initialize playwright: {e}")
508+
self.logger.error(f"Failed to initialize playwright in thread: {e}")
482509
raise RuntimeError(
483510
"Failed to initialize Playwright. This may indicate your environment "
484-
"has restrictions on subprocess creation or event loop operations."
511+
"has restrictions on subprocess creation or threading operations."
485512
) from e
486513

487514
def agent(self, **kwargs) -> Agent:

0 commit comments

Comments
 (0)