@@ -210,26 +210,47 @@ def cleanup_handler(sig, frame):
210
210
# Try to get the current event loop
211
211
try :
212
212
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
+
213
223
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 ..." )
216
226
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
+
218
251
except Exception as e :
219
252
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 )
233
254
234
255
except Exception as e :
235
256
print (f"Error during signal cleanup: { str (e )} " )
@@ -454,34 +475,40 @@ async def init(self):
454
475
455
476
async def _init_playwright_with_timeout (self ):
456
477
"""
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.
458
479
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.
461
483
"""
462
- self .logger .debug ("Starting playwright initialization with timeout ..." )
484
+ self .logger .debug ("Starting playwright initialization in separate thread ..." )
463
485
464
486
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 ()
470
500
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" )
472
505
return playwright_instance
473
506
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
480
507
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 } " )
482
509
raise RuntimeError (
483
510
"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."
485
512
) from e
486
513
487
514
def agent (self , ** kwargs ) -> Agent :
0 commit comments