diff -r a6bc96c2b8be Lib/asyncio/base_events.py --- a/Lib/asyncio/base_events.py Tue Nov 17 00:19:27 2015 +0000 +++ b/Lib/asyncio/base_events.py Tue Nov 17 02:54:55 2015 -0700 @@ -70,10 +70,6 @@ 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. @@ -118,9 +114,6 @@ "got host %r: %s" % (host, err)) -def _raise_stop_error(*args): - raise _StopError - def _run_until_complete_cb(fut): exc = fut._exception @@ -129,7 +122,7 @@ # Issue #22429: run_forever() already finished, no need to # stop it. return - _raise_stop_error() + fut._loop.stop() class Server(events.AbstractServer): @@ -184,6 +177,7 @@ def __init__(self): self._timer_cancelled_count = 0 self._closed = False + self._stopped = None self._ready = collections.deque() self._scheduled = [] self._default_executor = None @@ -295,13 +289,11 @@ if self.is_running(): raise RuntimeError('Event loop is running.') self._set_coroutine_wrapper(self._debug) + self._stopped = False self._thread_id = threading.get_ident() try: - while True: - try: - self._run_once() - except _StopError: - break + while not self._stopped: + self._run_once() finally: self._thread_id = None self._set_coroutine_wrapper(False) @@ -345,11 +337,10 @@ 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._stopped = True def close(self): """Close the event loop. diff -r a6bc96c2b8be Lib/asyncio/test_utils.py --- a/Lib/asyncio/test_utils.py Tue Nov 17 00:19:27 2015 +0000 +++ b/Lib/asyncio/test_utils.py Tue Nov 17 02:54:55 2015 -0700 @@ -70,16 +70,6 @@ loop.run_until_complete(tasks.sleep(0.001, loop=loop)) -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. - """ - loop.stop() - loop.run_forever() - - class SilentWSGIRequestHandler(WSGIRequestHandler): def get_stderr(self): diff -r a6bc96c2b8be Lib/test/test_asyncio/test_base_events.py --- a/Lib/test/test_asyncio/test_base_events.py Tue Nov 17 00:19:27 2015 +0000 +++ b/Lib/test/test_asyncio/test_base_events.py Tue Nov 17 02:54:55 2015 -0700 @@ -757,6 +757,36 @@ 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_setinel = object() + callcount = 0 + doer = None + + def proc_events(event_list): + nonlocal doer + if event_setinel 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_setinel,) + + 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) + class MyProto(asyncio.Protocol): done = None