From 8726cbda0fa8136fcb97bc9ba80db39b968f647e Mon Sep 17 00:00:00 2001 From: Ashi Date: Mon, 13 Apr 2026 21:38:58 +0300 Subject: [PATCH] Fix daemon thread, is_same_loop detection, and run_threadsafe timeout handling - Set browser thread as daemon=True so it doesn't block process exit - Replace get_event_loop() with get_running_loop() for correct same-loop detection - Always signal completion event in finally block to prevent deadlock on task failure - Raise TimeoutError and cancel task when run_threadsafe exceeds timeout Co-Authored-By: Claude Sonnet 4.6 --- .../browser/threadsafe_browser.py | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/PlaywrightSafeThread/browser/threadsafe_browser.py b/PlaywrightSafeThread/browser/threadsafe_browser.py index fd40d20..36e899a 100644 --- a/PlaywrightSafeThread/browser/threadsafe_browser.py +++ b/PlaywrightSafeThread/browser/threadsafe_browser.py @@ -273,9 +273,8 @@ def __init__( self.loop = asyncio.new_event_loop() self.start_event = Event() - # self.thread = Thread(target=self.__thread_worker, daemon=True) self.thread = Thread( - name="Thread-browser-%i" % id(self), target=self.__thread_worker + name="Thread-browser-%i" % id(self), target=self.__thread_worker, daemon=True ) # TODO:: @@ -314,12 +313,14 @@ async def create_task(self, task, *args, **kwargs): @property def is_same_loop(self): + # Use get_running_loop() — the modern Python 3.7+ API. + # Returns the loop that is *currently executing* in this thread. + # Raises RuntimeError when no loop is running (e.g. a plain worker + # thread), in which case we are definitely not on the same loop. try: - return asyncio.get_event_loop() == self.loop - except Exception as e: - if 'There is no current event loop in thread' in str(e): - return True - raise e + return asyncio.get_running_loop() == self.loop + except RuntimeError: + return False def run_threadsafe(self, task, *args, timeout_=120, **kwargs): if not asyncio.iscoroutine(task): @@ -340,21 +341,25 @@ def run_threadsafe(self, task, *args, timeout_=120, **kwargs): # result = future.result(timeout=timeout_) # return result - # TODO :: use __handle_future start_event_task = Event() async def run_task(task_): - r = await task_ - start_event_task.set() - return r - - future = self.loop.create_task( - run_task(task) - ) - - start_event_task.wait(timeout_) - result = future.result() - return result + try: + return await task_ + finally: + # Always signal completion — even when the task raises, + # so the waiting thread is never left blocking until timeout. + start_event_task.set() + + future = self.loop.create_task(run_task(task)) + + completed = start_event_task.wait(timeout_) + if not completed: + future.cancel() + raise TimeoutError(f"Task did not complete within {timeout_}s") + + # future.result() re-raises any exception the task threw + return future.result() async def __start_playwright(self) -> None: self.playwright = await async_playwright().start()