@@ -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