Skip to content

Commit 53d5e67

Browse files
gh-111358: Fix timeout behaviour in BaseEventLoop.shutdown_default_executor (#115622)
1 parent edea0e7 commit 53d5e67

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
@@ -598,23 +599,24 @@ async def shutdown_default_executor(self, timeout=None):
598599
thread = threading.Thread(target=self._do_shutdown, args=(future,))
599600
thread.start()
600601
try:
601-
await future
602-
finally:
603-
thread.join(timeout)
604-
605-
if thread.is_alive():
602+
async with timeouts.timeout(timeout):
603+
await future
604+
except TimeoutError:
606605
warnings.warn("The executor did not finishing joining "
607-
f"its threads within {timeout} seconds.",
608-
RuntimeWarning, stacklevel=2)
606+
f"its threads within {timeout} seconds.",
607+
RuntimeWarning, stacklevel=2)
609608
self._default_executor.shutdown(wait=False)
609+
else:
610+
thread.join()
610611

611612
def _do_shutdown(self, future):
612613
try:
613614
self._default_executor.shutdown(wait=True)
614615
if not self.is_closed():
615-
self.call_soon_threadsafe(future.set_result, None)
616+
self.call_soon_threadsafe(futures._set_result_unless_cancelled,
617+
future, None)
616618
except Exception as ex:
617-
if not self.is_closed():
619+
if not self.is_closed() and not future.cancelled():
618620
self.call_soon_threadsafe(future.set_exception, ex)
619621

620622
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)