Issue #25593: Change semantics of EventLoop.stop(). (Merge 3.5->3.6)

This commit is contained in:
Guido van Rossum 2015-11-19 13:34:24 -08:00
commit 13d9b86d46
6 changed files with 87 additions and 28 deletions

View File

@ -29,7 +29,16 @@ Run an event loop
.. method:: BaseEventLoop.run_forever() .. method:: BaseEventLoop.run_forever()
Run until :meth:`stop` is called. Run until :meth:`stop` is called. If :meth:`stop` is called before
:meth:`run_forever()` is called, this polls the I/O selector once
with a timeout of zero, runs all callbacks scheduled in response to
I/O events (and those that were already scheduled), and then exits.
If :meth:`stop` is called while :meth:`run_forever` is running,
this will run the current batch of callbacks and then exit. Note
that callbacks scheduled by callbacks will not run in that case;
they will run the next time :meth:`run_forever` is called.
.. versionchanged:: 3.5.1
.. method:: BaseEventLoop.run_until_complete(future) .. method:: BaseEventLoop.run_until_complete(future)
@ -48,10 +57,10 @@ Run an event loop
Stop running the event loop. Stop running the event loop.
Every callback scheduled before :meth:`stop` is called will run. This causes :meth:`run_forever` to exit at the next suitable
Callbacks scheduled after :meth:`stop` is called will not run. opportunity (see there for more details).
However, those callbacks will run if :meth:`run_forever` is called
again later. .. versionchanged:: 3.5.1
.. method:: BaseEventLoop.is_closed() .. method:: BaseEventLoop.is_closed()
@ -61,7 +70,8 @@ Run an event loop
.. method:: BaseEventLoop.close() .. method:: BaseEventLoop.close()
Close the event loop. The loop must not be running. Close the event loop. The loop must not be running. Pending
callbacks will be lost.
This clears the queues and shuts down the executor, but does not wait for This clears the queues and shuts down the executor, but does not wait for
the executor to finish. the executor to finish.

View File

@ -494,7 +494,7 @@ data and wait until the connection is closed::
def connection_lost(self, exc): def connection_lost(self, exc):
print('The server closed the connection') print('The server closed the connection')
print('Stop the event lop') print('Stop the event loop')
self.loop.stop() self.loop.stop()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View File

@ -70,10 +70,6 @@ def _format_pipe(fd):
return repr(fd) return repr(fd)
class _StopError(BaseException):
"""Raised to stop the event loop."""
def _check_resolved_address(sock, address): def _check_resolved_address(sock, address):
# Ensure that the address is already resolved to avoid the trap of hanging # Ensure that the address is already resolved to avoid the trap of hanging
# the entire event loop when the address requires doing a DNS lookup. # the entire event loop when the address requires doing a DNS lookup.
@ -118,9 +114,6 @@ def _check_resolved_address(sock, address):
"got host %r: %s" "got host %r: %s"
% (host, err)) % (host, err))
def _raise_stop_error(*args):
raise _StopError
def _run_until_complete_cb(fut): def _run_until_complete_cb(fut):
exc = fut._exception exc = fut._exception
@ -129,7 +122,7 @@ def _run_until_complete_cb(fut):
# Issue #22429: run_forever() already finished, no need to # Issue #22429: run_forever() already finished, no need to
# stop it. # stop it.
return return
_raise_stop_error() fut._loop.stop()
class Server(events.AbstractServer): class Server(events.AbstractServer):
@ -184,6 +177,7 @@ class BaseEventLoop(events.AbstractEventLoop):
def __init__(self): def __init__(self):
self._timer_cancelled_count = 0 self._timer_cancelled_count = 0
self._closed = False self._closed = False
self._stopping = False
self._ready = collections.deque() self._ready = collections.deque()
self._scheduled = [] self._scheduled = []
self._default_executor = None self._default_executor = None
@ -298,11 +292,11 @@ class BaseEventLoop(events.AbstractEventLoop):
self._thread_id = threading.get_ident() self._thread_id = threading.get_ident()
try: try:
while True: while True:
try:
self._run_once() self._run_once()
except _StopError: if self._stopping:
break break
finally: finally:
self._stopping = False
self._thread_id = None self._thread_id = None
self._set_coroutine_wrapper(False) self._set_coroutine_wrapper(False)
@ -345,11 +339,10 @@ class BaseEventLoop(events.AbstractEventLoop):
def stop(self): def stop(self):
"""Stop running the event loop. """Stop running the event loop.
Every callback scheduled before stop() is called will run. Callbacks Every callback already scheduled will still run. This simply informs
scheduled after stop() is called will not run. However, those callbacks run_forever to stop looping after a complete iteration.
will run if run_forever is called again later.
""" """
self.call_soon(_raise_stop_error) self._stopping = True
def close(self): def close(self):
"""Close the event loop. """Close the event loop.
@ -1194,7 +1187,7 @@ class BaseEventLoop(events.AbstractEventLoop):
handle._scheduled = False handle._scheduled = False
timeout = None timeout = None
if self._ready: if self._ready or self._stopping:
timeout = 0 timeout = 0
elif self._scheduled: elif self._scheduled:
# Compute the desired timeout. # Compute the desired timeout.

View File

@ -71,12 +71,13 @@ def run_until(loop, pred, timeout=30):
def run_once(loop): def run_once(loop):
"""loop.stop() schedules _raise_stop_error() """Legacy API to run once through the event loop.
and run_forever() runs until _raise_stop_error() callback.
this wont work if test waits for some IO events, because This is the recommended pattern for test code. It will poll the
_raise_stop_error() runs before any of io events callbacks. selector once and run all callbacks scheduled in response to I/O
events.
""" """
loop.stop() loop.call_soon(loop.stop)
loop.run_forever() loop.run_forever()

View File

@ -757,6 +757,59 @@ class BaseEventLoopTests(test_utils.TestCase):
pass pass
self.assertTrue(func.called) self.assertTrue(func.called)
def test_single_selecter_event_callback_after_stopping(self):
# Python issue #25593: A stopped event loop may cause event callbacks
# to run more than once.
event_sentinel = object()
callcount = 0
doer = None
def proc_events(event_list):
nonlocal doer
if event_sentinel in event_list:
doer = self.loop.call_soon(do_event)
def do_event():
nonlocal callcount
callcount += 1
self.loop.call_soon(clear_selector)
def clear_selector():
doer.cancel()
self.loop._selector.select.return_value = ()
self.loop._process_events = proc_events
self.loop._selector.select.return_value = (event_sentinel,)
for i in range(1, 3):
with self.subTest('Loop %d/2' % i):
self.loop.call_soon(self.loop.stop)
self.loop.run_forever()
self.assertEqual(callcount, 1)
def test_run_once(self):
# Simple test for test_utils.run_once(). It may seem strange
# to have a test for this (the function isn't even used!) but
# it's a de-factor standard API for library tests. This tests
# the idiom: loop.call_soon(loop.stop); loop.run_forever().
count = 0
def callback():
nonlocal count
count += 1
self.loop._process_events = mock.Mock()
self.loop.call_soon(callback)
test_utils.run_once(self.loop)
self.assertEqual(count, 1)
def test_run_forever_pre_stopped(self):
# Test that the old idiom for pre-stopping the loop works.
self.loop._process_events = mock.Mock()
self.loop.stop()
self.loop.run_forever()
self.loop._selector.select.assert_called_once_with(0)
class MyProto(asyncio.Protocol): class MyProto(asyncio.Protocol):
done = None done = None

View File

@ -95,6 +95,8 @@ Core and Builtins
Library Library
------- -------
- Issue #25593: Change semantics of EventLoop.stop() in asyncio.
- Issue #6973: When we know a subprocess.Popen process has died, do - Issue #6973: When we know a subprocess.Popen process has died, do
not allow the send_signal(), terminate(), or kill() methods to do not allow the send_signal(), terminate(), or kill() methods to do
anything as they could potentially signal a different process. anything as they could potentially signal a different process.