Skip to content

Commit

Permalink
Issue #25593: Change semantics of EventLoop.stop().
Browse files Browse the repository at this point in the history
  • Loading branch information
gvanrossum committed Nov 19, 2015
1 parent 01a65af commit 41f69f4
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 28 deletions.
22 changes: 16 additions & 6 deletions Doc/library/asyncio-eventloop.rst
Expand Up @@ -29,7 +29,16 @@ Run an event loop

.. 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.4.4

.. method:: BaseEventLoop.run_until_complete(future)

Expand All @@ -48,10 +57,10 @@ Run an event loop

Stop running the event loop.

Every callback scheduled before :meth:`stop` is called will run.
Callbacks scheduled after :meth:`stop` is called will not run.
However, those callbacks will run if :meth:`run_forever` is called
again later.
This causes :meth:`run_forever` to exit at the next suitable
opportunity (see there for more details).

.. versionchanged:: 3.4.4

.. method:: BaseEventLoop.is_closed()

Expand All @@ -61,7 +70,8 @@ Run an event loop

.. 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
the executor to finish.
Expand Down
2 changes: 1 addition & 1 deletion Doc/library/asyncio-protocol.rst
Expand Up @@ -494,7 +494,7 @@ data and wait until the connection is closed::

def connection_lost(self, exc):
print('The server closed the connection')
print('Stop the event lop')
print('Stop the event loop')
self.loop.stop()

loop = asyncio.get_event_loop()
Expand Down
25 changes: 9 additions & 16 deletions Lib/asyncio/base_events.py
Expand Up @@ -70,10 +70,6 @@ def _format_pipe(fd):
return repr(fd)


class _StopError(BaseException):
"""Raised to stop the event loop."""


def _check_resolved_address(sock, address):
# 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.
Expand Down Expand Up @@ -118,9 +114,6 @@ def _check_resolved_address(sock, address):
"got host %r: %s"
% (host, err))

def _raise_stop_error(*args):
raise _StopError


def _run_until_complete_cb(fut):
exc = fut._exception
Expand All @@ -129,7 +122,7 @@ def _run_until_complete_cb(fut):
# Issue #22429: run_forever() already finished, no need to
# stop it.
return
_raise_stop_error()
fut._loop.stop()


class Server(events.AbstractServer):
Expand Down Expand Up @@ -184,6 +177,7 @@ class BaseEventLoop(events.AbstractEventLoop):
def __init__(self):
self._timer_cancelled_count = 0
self._closed = False
self._stopping = False
self._ready = collections.deque()
self._scheduled = []
self._default_executor = None
Expand Down Expand Up @@ -298,11 +292,11 @@ def run_forever(self):
self._thread_id = threading.get_ident()
try:
while True:
try:
self._run_once()
except _StopError:
self._run_once()
if self._stopping:
break
finally:
self._stopping = False
self._thread_id = None
self._set_coroutine_wrapper(False)

Expand Down Expand Up @@ -345,11 +339,10 @@ def run_until_complete(self, future):
def stop(self):
"""Stop running the event loop.
Every callback scheduled before stop() is called will run. Callbacks
scheduled after stop() is called will not run. However, those callbacks
will run if run_forever is called again later.
Every callback already scheduled will still run. This simply informs
run_forever to stop looping after a complete iteration.
"""
self.call_soon(_raise_stop_error)
self._stopping = True

def close(self):
"""Close the event loop.
Expand Down Expand Up @@ -1194,7 +1187,7 @@ def _run_once(self):
handle._scheduled = False

timeout = None
if self._ready:
if self._ready or self._stopping:
timeout = 0
elif self._scheduled:
# Compute the desired timeout.
Expand Down
11 changes: 6 additions & 5 deletions Lib/asyncio/test_utils.py
Expand Up @@ -71,12 +71,13 @@ def run_until(loop, pred, timeout=30):


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


Expand Down
53 changes: 53 additions & 0 deletions Lib/test/test_asyncio/test_base_events.py
Expand Up @@ -757,6 +757,59 @@ def func():
pass
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):
done = None
Expand Down
2 changes: 2 additions & 0 deletions Misc/NEWS
Expand Up @@ -106,6 +106,8 @@ Core and Builtins
Library
-------

- Issue #25593: Change semantics of EventLoop.stop() in asyncio.

- Issue #6973: When we know a subprocess.Popen process has died, do
not allow the send_signal(), terminate(), or kill() methods to do
anything as they could potentially signal a different process.
Expand Down

0 comments on commit 41f69f4

Please sign in to comment.