Skip to content

Commit e25ac3b

Browse files
[3.12] gh-111358: Fix timeout behaviour in BaseEventLoop.shutdown_default_executor (GH-115622) (#115641)
(cherry picked from commit 53d5e67) Co-authored-by: Jamie Phan <jamie@ordinarylab.dev>
1 parent ae6c01d commit e25ac3b

File tree

3 files changed

+29
-9
lines changed

3 files changed

+29
-9
lines changed

Lib/asyncio/base_events.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from . import sslproto
4646
from . import staggered
4747
from . import tasks
48+
from . import timeouts
4849
from . import transports
4950
from . import trsock
5051
from .log import logger
@@ -596,23 +597,24 @@ async def shutdown_default_executor(self, timeout=None):
596597
thread = threading.Thread(target=self._do_shutdown, args=(future,))
597598
thread.start()
598599
try:
599-
await future
600-
finally:
601-
thread.join(timeout)
602-
603-
if thread.is_alive():
600+
async with timeouts.timeout(timeout):
601+
await future
602+
except TimeoutError:
604603
warnings.warn("The executor did not finishing joining "
605-
f"its threads within {timeout} seconds.",
606-
RuntimeWarning, stacklevel=2)
604+
f"its threads within {timeout} seconds.",
605+
RuntimeWarning, stacklevel=2)
607606
self._default_executor.shutdown(wait=False)
607+
else:
608+
thread.join()
608609

609610
def _do_shutdown(self, future):
610611
try:
611612
self._default_executor.shutdown(wait=True)
612613
if not self.is_closed():
613-
self.call_soon_threadsafe(future.set_result, None)
614+
self.call_soon_threadsafe(futures._set_result_unless_cancelled,
615+
future, None)
614616
except Exception as ex:
615-
if not self.is_closed():
617+
if not self.is_closed() and not future.cancelled():
616618
self.call_soon_threadsafe(future.set_exception, ex)
617619

618620
def _check_running(self):

Lib/test/test_asyncio/test_base_events.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,22 @@ def test_set_default_executor_error(self):
231231

232232
self.assertIsNone(self.loop._default_executor)
233233

234+
def test_shutdown_default_executor_timeout(self):
235+
class DummyExecutor(concurrent.futures.ThreadPoolExecutor):
236+
def shutdown(self, wait=True, *, cancel_futures=False):
237+
if wait:
238+
time.sleep(0.1)
239+
240+
self.loop._process_events = mock.Mock()
241+
self.loop._write_to_self = mock.Mock()
242+
executor = DummyExecutor()
243+
self.loop.set_default_executor(executor)
244+
245+
with self.assertWarnsRegex(RuntimeWarning,
246+
"The executor did not finishing joining"):
247+
self.loop.run_until_complete(
248+
self.loop.shutdown_default_executor(timeout=0.01))
249+
234250
def test_call_soon(self):
235251
def cb():
236252
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a bug in :meth:`asyncio.BaseEventLoop.shutdown_default_executor` to
2+
ensure the timeout passed to the coroutine behaves as expected.

0 commit comments

Comments
 (0)