GH-82448: Add thread timeout for loop.shutdown_default_executor (#97561)

Co-authored-by: Kyle Stanley <aeros167@gmail.com>
This commit is contained in:
Kumar Aditya 2022-09-28 23:09:42 +05:30 committed by GitHub
parent 9a404b173e
commit 575a253b5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 44 additions and 9 deletions

View File

@ -180,18 +180,27 @@ Running and stopping the loop
.. versionadded:: 3.6
.. coroutinemethod:: loop.shutdown_default_executor()
.. coroutinemethod:: loop.shutdown_default_executor(timeout=None)
Schedule the closure of the default executor and wait for it to join all of
the threads in the :class:`ThreadPoolExecutor`. After calling this method, a
:exc:`RuntimeError` will be raised if :meth:`loop.run_in_executor` is called
while using the default executor.
The *timeout* parameter specifies the amount of time the executor will
be given to finish joining. The default value is ``None``, which means the
executor will be given an unlimited amount of time.
If the timeout duration is reached, a warning is emitted and executor is
terminated without waiting for its threads to finish joining.
Note that there is no need to call this function when
:func:`asyncio.run` is used.
.. versionadded:: 3.9
.. versionchanged:: 3.12
Added the *timeout* parameter.
Scheduling callbacks
^^^^^^^^^^^^^^^^^^^^

View File

@ -28,7 +28,7 @@ Running an asyncio Program
This function runs the passed coroutine, taking care of
managing the asyncio event loop, *finalizing asynchronous
generators*, and closing the threadpool.
generators*, and closing the executor.
This function cannot be called when another asyncio event loop is
running in the same thread.
@ -41,6 +41,10 @@ Running an asyncio Program
the end. It should be used as a main entry point for asyncio
programs, and should ideally only be called once.
The executor is given a timeout duration of 5 minutes to shutdown.
If the executor hasn't finished within that duration, a warning is
emitted and the executor is closed.
Example::
async def main():

View File

@ -561,8 +561,13 @@ class BaseEventLoop(events.AbstractEventLoop):
'asyncgen': agen
})
async def shutdown_default_executor(self):
"""Schedule the shutdown of the default executor."""
async def shutdown_default_executor(self, timeout=None):
"""Schedule the shutdown of the default executor.
The timeout parameter specifies the amount of time the executor will
be given to finish joining. The default value is None, which means
that the executor will be given an unlimited amount of time.
"""
self._executor_shutdown_called = True
if self._default_executor is None:
return
@ -572,7 +577,13 @@ class BaseEventLoop(events.AbstractEventLoop):
try:
await future
finally:
thread.join()
thread.join(timeout)
if thread.is_alive():
warnings.warn("The executor did not finishing joining "
f"its threads within {timeout} seconds.",
RuntimeWarning, stacklevel=2)
self._default_executor.shutdown(wait=False)
def _do_shutdown(self, future):
try:

View File

@ -26,6 +26,9 @@ SENDFILE_FALLBACK_READBUFFER_SIZE = 1024 * 256
FLOW_CONTROL_HIGH_WATER_SSL_READ = 256 # KiB
FLOW_CONTROL_HIGH_WATER_SSL_WRITE = 512 # KiB
# Default timeout for joining the threads in the threadpool
THREAD_JOIN_TIMEOUT = 300
# The enum should be here to break circular dependencies between
# base_events and sslproto
class _SendfileMode(enum.Enum):

View File

@ -9,7 +9,7 @@ from . import coroutines
from . import events
from . import exceptions
from . import tasks
from . import constants
class _State(enum.Enum):
CREATED = "created"
@ -69,7 +69,8 @@ class Runner:
loop = self._loop
_cancel_all_tasks(loop)
loop.run_until_complete(loop.shutdown_asyncgens())
loop.run_until_complete(loop.shutdown_default_executor())
loop.run_until_complete(
loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT))
finally:
if self._set_event_loop:
events.set_event_loop(None)
@ -160,8 +161,8 @@ def run(main, *, debug=None):
"""Execute the coroutine and return the result.
This function runs the passed coroutine, taking care of
managing the asyncio event loop and finalizing asynchronous
generators.
managing the asyncio event loop, finalizing asynchronous
generators and closing the default executor.
This function cannot be called when another asyncio event loop is
running in the same thread.
@ -172,6 +173,10 @@ def run(main, *, debug=None):
It should be used as a main entry point for asyncio programs, and should
ideally only be called once.
The executor is given a timeout duration of 5 minutes to shutdown.
If the executor hasn't finished within that duration, a warning is
emitted and the executor is closed.
Example:
async def main():

View File

@ -0,0 +1,3 @@
Add *timeout* parameter to :meth:`asyncio.loop.shutdown_default_executor`.
The default value is ``None``, which means the executor will be given an unlimited amount of time.
When called from :class:`asyncio.Runner` or :func:`asyncio.run`, the default timeout is 5 minutes.