]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-111358: Fix timeout behaviour in BaseEventLoop.shutdown_default_executor (#115622)
authorJamie Phan <jamie@ordinarylab.dev>
Mon, 19 Feb 2024 00:01:00 +0000 (11:01 +1100)
committerGitHub <noreply@github.com>
Mon, 19 Feb 2024 00:01:00 +0000 (00:01 +0000)
Lib/asyncio/base_events.py
Lib/test/test_asyncio/test_base_events.py
Misc/NEWS.d/next/Library/2024-02-18-12-18-12.gh-issue-111358.9yJUMD.rst [new file with mode: 0644]

index aadc4f478f8b560ffadc1b89721fd6eb967f3c8c..6c5cf28e7c59d42fdd5088a2d6059e549c65d5e7 100644 (file)
@@ -45,6 +45,7 @@ from . import protocols
 from . import sslproto
 from . import staggered
 from . import tasks
+from . import timeouts
 from . import transports
 from . import trsock
 from .log import logger
@@ -598,23 +599,24 @@ class BaseEventLoop(events.AbstractEventLoop):
         thread = threading.Thread(target=self._do_shutdown, args=(future,))
         thread.start()
         try:
-            await future
-        finally:
-            thread.join(timeout)
-
-        if thread.is_alive():
+            async with timeouts.timeout(timeout):
+                await future
+        except TimeoutError:
             warnings.warn("The executor did not finishing joining "
-                             f"its threads within {timeout} seconds.",
-                             RuntimeWarning, stacklevel=2)
+                          f"its threads within {timeout} seconds.",
+                          RuntimeWarning, stacklevel=2)
             self._default_executor.shutdown(wait=False)
+        else:
+            thread.join()
 
     def _do_shutdown(self, future):
         try:
             self._default_executor.shutdown(wait=True)
             if not self.is_closed():
-                self.call_soon_threadsafe(future.set_result, None)
+                self.call_soon_threadsafe(futures._set_result_unless_cancelled,
+                                          future, None)
         except Exception as ex:
-            if not self.is_closed():
+            if not self.is_closed() and not future.cancelled():
                 self.call_soon_threadsafe(future.set_exception, ex)
 
     def _check_running(self):
index 82071edb2525706f5c643d73dc70aaadf6005960..4cd872d3a5b2d891f9c926307c899d67f267114b 100644 (file)
@@ -231,6 +231,22 @@ class BaseEventLoopTests(test_utils.TestCase):
 
         self.assertIsNone(self.loop._default_executor)
 
+    def test_shutdown_default_executor_timeout(self):
+        class DummyExecutor(concurrent.futures.ThreadPoolExecutor):
+            def shutdown(self, wait=True, *, cancel_futures=False):
+                if wait:
+                    time.sleep(0.1)
+
+        self.loop._process_events = mock.Mock()
+        self.loop._write_to_self = mock.Mock()
+        executor = DummyExecutor()
+        self.loop.set_default_executor(executor)
+
+        with self.assertWarnsRegex(RuntimeWarning,
+                                   "The executor did not finishing joining"):
+            self.loop.run_until_complete(
+                self.loop.shutdown_default_executor(timeout=0.01))
+
     def test_call_soon(self):
         def cb():
             pass
diff --git a/Misc/NEWS.d/next/Library/2024-02-18-12-18-12.gh-issue-111358.9yJUMD.rst b/Misc/NEWS.d/next/Library/2024-02-18-12-18-12.gh-issue-111358.9yJUMD.rst
new file mode 100644 (file)
index 0000000..2e895f8
--- /dev/null
@@ -0,0 +1,2 @@
+Fix a bug in :meth:`asyncio.BaseEventLoop.shutdown_default_executor` to
+ensure the timeout passed to the coroutine behaves as expected.