Skip to content

Commit 41f69f4

Browse files
committed
Issue #25593: Change semantics of EventLoop.stop().
1 parent 01a65af commit 41f69f4

File tree

6 files changed

+87
-28
lines changed

6 files changed

+87
-28
lines changed

Doc/library/asyncio-eventloop.rst

+16-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ Run an event loop
2929

3030
.. method:: BaseEventLoop.run_forever()
3131

32-
Run until :meth:`stop` is called.
32+
Run until :meth:`stop` is called. If :meth:`stop` is called before
33+
:meth:`run_forever()` is called, this polls the I/O selector once
34+
with a timeout of zero, runs all callbacks scheduled in response to
35+
I/O events (and those that were already scheduled), and then exits.
36+
If :meth:`stop` is called while :meth:`run_forever` is running,
37+
this will run the current batch of callbacks and then exit. Note
38+
that callbacks scheduled by callbacks will not run in that case;
39+
they will run the next time :meth:`run_forever` is called.
40+
41+
.. versionchanged:: 3.4.4
3342

3443
.. method:: BaseEventLoop.run_until_complete(future)
3544

@@ -48,10 +57,10 @@ Run an event loop
4857

4958
Stop running the event loop.
5059

51-
Every callback scheduled before :meth:`stop` is called will run.
52-
Callbacks scheduled after :meth:`stop` is called will not run.
53-
However, those callbacks will run if :meth:`run_forever` is called
54-
again later.
60+
This causes :meth:`run_forever` to exit at the next suitable
61+
opportunity (see there for more details).
62+
63+
.. versionchanged:: 3.4.4
5564

5665
.. method:: BaseEventLoop.is_closed()
5766

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

6271
.. method:: BaseEventLoop.close()
6372

64-
Close the event loop. The loop must not be running.
73+
Close the event loop. The loop must not be running. Pending
74+
callbacks will be lost.
6575

6676
This clears the queues and shuts down the executor, but does not wait for
6777
the executor to finish.

Doc/library/asyncio-protocol.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ data and wait until the connection is closed::
494494

495495
def connection_lost(self, exc):
496496
print('The server closed the connection')
497-
print('Stop the event lop')
497+
print('Stop the event loop')
498498
self.loop.stop()
499499

500500
loop = asyncio.get_event_loop()

Lib/asyncio/base_events.py

+9-16
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ def _format_pipe(fd):
7070
return repr(fd)
7171

7272

73-
class _StopError(BaseException):
74-
"""Raised to stop the event loop."""
75-
76-
7773
def _check_resolved_address(sock, address):
7874
# Ensure that the address is already resolved to avoid the trap of hanging
7975
# the entire event loop when the address requires doing a DNS lookup.
@@ -118,9 +114,6 @@ def _check_resolved_address(sock, address):
118114
"got host %r: %s"
119115
% (host, err))
120116

121-
def _raise_stop_error(*args):
122-
raise _StopError
123-
124117

125118
def _run_until_complete_cb(fut):
126119
exc = fut._exception
@@ -129,7 +122,7 @@ def _run_until_complete_cb(fut):
129122
# Issue #22429: run_forever() already finished, no need to
130123
# stop it.
131124
return
132-
_raise_stop_error()
125+
fut._loop.stop()
133126

134127

135128
class Server(events.AbstractServer):
@@ -184,6 +177,7 @@ class BaseEventLoop(events.AbstractEventLoop):
184177
def __init__(self):
185178
self._timer_cancelled_count = 0
186179
self._closed = False
180+
self._stopping = False
187181
self._ready = collections.deque()
188182
self._scheduled = []
189183
self._default_executor = None
@@ -298,11 +292,11 @@ def run_forever(self):
298292
self._thread_id = threading.get_ident()
299293
try:
300294
while True:
301-
try:
302-
self._run_once()
303-
except _StopError:
295+
self._run_once()
296+
if self._stopping:
304297
break
305298
finally:
299+
self._stopping = False
306300
self._thread_id = None
307301
self._set_coroutine_wrapper(False)
308302

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

354347
def close(self):
355348
"""Close the event loop.
@@ -1194,7 +1187,7 @@ def _run_once(self):
11941187
handle._scheduled = False
11951188

11961189
timeout = None
1197-
if self._ready:
1190+
if self._ready or self._stopping:
11981191
timeout = 0
11991192
elif self._scheduled:
12001193
# Compute the desired timeout.

Lib/asyncio/test_utils.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ def run_until(loop, pred, timeout=30):
7171

7272

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

8283

Lib/test/test_asyncio/test_base_events.py

+53
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,59 @@ def func():
757757
pass
758758
self.assertTrue(func.called)
759759

760+
def test_single_selecter_event_callback_after_stopping(self):
761+
# Python issue #25593: A stopped event loop may cause event callbacks
762+
# to run more than once.
763+
event_sentinel = object()
764+
callcount = 0
765+
doer = None
766+
767+
def proc_events(event_list):
768+
nonlocal doer
769+
if event_sentinel in event_list:
770+
doer = self.loop.call_soon(do_event)
771+
772+
def do_event():
773+
nonlocal callcount
774+
callcount += 1
775+
self.loop.call_soon(clear_selector)
776+
777+
def clear_selector():
778+
doer.cancel()
779+
self.loop._selector.select.return_value = ()
780+
781+
self.loop._process_events = proc_events
782+
self.loop._selector.select.return_value = (event_sentinel,)
783+
784+
for i in range(1, 3):
785+
with self.subTest('Loop %d/2' % i):
786+
self.loop.call_soon(self.loop.stop)
787+
self.loop.run_forever()
788+
self.assertEqual(callcount, 1)
789+
790+
def test_run_once(self):
791+
# Simple test for test_utils.run_once(). It may seem strange
792+
# to have a test for this (the function isn't even used!) but
793+
# it's a de-factor standard API for library tests. This tests
794+
# the idiom: loop.call_soon(loop.stop); loop.run_forever().
795+
count = 0
796+
797+
def callback():
798+
nonlocal count
799+
count += 1
800+
801+
self.loop._process_events = mock.Mock()
802+
self.loop.call_soon(callback)
803+
test_utils.run_once(self.loop)
804+
self.assertEqual(count, 1)
805+
806+
def test_run_forever_pre_stopped(self):
807+
# Test that the old idiom for pre-stopping the loop works.
808+
self.loop._process_events = mock.Mock()
809+
self.loop.stop()
810+
self.loop.run_forever()
811+
self.loop._selector.select.assert_called_once_with(0)
812+
760813

761814
class MyProto(asyncio.Protocol):
762815
done = None

Misc/NEWS

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ Core and Builtins
106106
Library
107107
-------
108108

109+
- Issue #25593: Change semantics of EventLoop.stop() in asyncio.
110+
109111
- Issue #6973: When we know a subprocess.Popen process has died, do
110112
not allow the send_signal(), terminate(), or kill() methods to do
111113
anything as they could potentially signal a different process.

0 commit comments

Comments
 (0)