From bb2fc5b2a58993c80ee81f10fe463039520a4162 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 10 Jun 2014 10:23:10 +0200 Subject: [PATCH] Issue #21326: Add a new is_closed() method to asyncio.BaseEventLoop Add BaseEventLoop._closed attribute and use it to check if the event loop was closed or not, instead of checking different attributes in each subclass of BaseEventLoop. run_forever() and run_until_complete() methods now raise a RuntimeError('Event loop is closed') exception if the event loop was closed. BaseProactorEventLoop.close() now also cancels "accept futures". --- Doc/library/asyncio-eventloop.rst | 6 +++++ Lib/asyncio/base_events.py | 19 +++++++++++++++ Lib/asyncio/proactor_events.py | 23 ++++++++++++------- Lib/asyncio/selector_events.py | 16 ++++++------- Lib/test/test_asyncio/test_base_events.py | 14 +++++++++++ Lib/test/test_asyncio/test_selector_events.py | 17 +++++++++++--- Misc/NEWS | 4 ++++ 7 files changed, 80 insertions(+), 19 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 6d0e617e4bb..7717b7a1c71 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -119,6 +119,12 @@ Run an event loop Callback scheduled after :meth:`stop` is called won't. However, those callbacks will run if :meth:`run_forever` is called again later. +.. method:: BaseEventLoop.is_closed() + + Returns ``True`` if the event loop was closed. + + .. versionadded:: 3.4.2 + .. method:: BaseEventLoop.close() Close the event loop. The loop should not be running. diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 1c7073c3703..5ee21d1cd11 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -119,6 +119,7 @@ class Server(events.AbstractServer): class BaseEventLoop(events.AbstractEventLoop): def __init__(self): + self._closed = False self._ready = collections.deque() self._scheduled = [] self._default_executor = None @@ -128,6 +129,11 @@ class BaseEventLoop(events.AbstractEventLoop): self._exception_handler = None self._debug = False + def __repr__(self): + return ('<%s running=%s closed=%s debug=%s>' + % (self.__class__.__name__, self.is_running(), + self.is_closed(), self.get_debug())) + def _make_socket_transport(self, sock, protocol, waiter=None, *, extra=None, server=None): """Create socket transport.""" @@ -173,8 +179,13 @@ class BaseEventLoop(events.AbstractEventLoop): """Process selector events.""" raise NotImplementedError + def _check_closed(self): + if self._closed: + raise RuntimeError('Event loop is closed') + def run_forever(self): """Run until stop() is called.""" + self._check_closed() if self._running: raise RuntimeError('Event loop is running.') self._running = True @@ -198,6 +209,7 @@ class BaseEventLoop(events.AbstractEventLoop): Return the Future's result, or raise its exception. """ + self._check_closed() future = tasks.async(future, loop=self) future.add_done_callback(_raise_stop_error) self.run_forever() @@ -222,6 +234,9 @@ class BaseEventLoop(events.AbstractEventLoop): This clears the queues and shuts down the executor, but does not wait for the executor to finish. """ + if self._closed: + return + self._closed = True self._ready.clear() self._scheduled.clear() executor = self._default_executor @@ -229,6 +244,10 @@ class BaseEventLoop(events.AbstractEventLoop): self._default_executor = None executor.shutdown(wait=False) + def is_closed(self): + """Returns True if the event loop was closed.""" + return self._closed + def is_running(self): """Returns running status of event loop.""" return self._running diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index d99e8ce7271..757a22e83ab 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -353,13 +353,14 @@ class BaseProactorEventLoop(base_events.BaseEventLoop): sock, protocol, waiter, extra) def close(self): - if self._proactor is not None: - self._close_self_pipe() - self._proactor.close() - self._proactor = None - self._selector = None - super().close() - self._accept_futures.clear() + if self.is_closed(): + return + self._stop_accept_futures() + self._close_self_pipe() + self._proactor.close() + self._proactor = None + self._selector = None + super().close() def sock_recv(self, sock, n): return self._proactor.recv(sock, n) @@ -428,6 +429,8 @@ class BaseProactorEventLoop(base_events.BaseEventLoop): self._make_socket_transport( conn, protocol, extra={'peername': addr}, server=server) + if self.is_closed(): + return f = self._proactor.accept(sock) except OSError as exc: if sock.fileno() != -1: @@ -448,8 +451,12 @@ class BaseProactorEventLoop(base_events.BaseEventLoop): def _process_events(self, event_list): pass # XXX hard work currently done in poll - def _stop_serving(self, sock): + def _stop_accept_futures(self): for future in self._accept_futures.values(): future.cancel() + self._accept_futures.clear() + + def _stop_serving(self, sock): + self._stop_accept_futures() self._proactor._stop_serving(sock) sock.close() diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 86a8d23c09a..1f8e5c8bf6d 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -55,11 +55,13 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop): return _SelectorDatagramTransport(self, sock, protocol, address, extra) def close(self): + if self.is_closed(): + return + self._close_self_pipe() if self._selector is not None: - self._close_self_pipe() self._selector.close() self._selector = None - super().close() + super().close() def _socketpair(self): raise NotImplementedError @@ -143,8 +145,7 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop): def add_reader(self, fd, callback, *args): """Add a reader callback.""" - if self._selector is None: - raise RuntimeError('Event loop is closed') + self._check_closed() handle = events.Handle(callback, args, self) try: key = self._selector.get_key(fd) @@ -160,7 +161,7 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop): def remove_reader(self, fd): """Remove a reader callback.""" - if self._selector is None: + if self.is_closed(): return False try: key = self._selector.get_key(fd) @@ -182,8 +183,7 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop): def add_writer(self, fd, callback, *args): """Add a writer callback..""" - if self._selector is None: - raise RuntimeError('Event loop is closed') + self._check_closed() handle = events.Handle(callback, args, self) try: key = self._selector.get_key(fd) @@ -199,7 +199,7 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop): def remove_writer(self, fd): """Remove a writer callback.""" - if self._selector is None: + if self.is_closed(): return False try: key = self._selector.get_key(fd) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index e28c32724aa..1611a11438a 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -52,6 +52,20 @@ class BaseEventLoopTests(unittest.TestCase): gen = self.loop._make_subprocess_transport(m, m, m, m, m, m, m) self.assertRaises(NotImplementedError, next, iter(gen)) + def test_close(self): + self.assertFalse(self.loop.is_closed()) + self.loop.close() + self.assertTrue(self.loop.is_closed()) + + # it should be possible to call close() more than once + self.loop.close() + self.loop.close() + + # operation blocked when the loop is closed + f = asyncio.Future(loop=self.loop) + self.assertRaises(RuntimeError, self.loop.run_forever) + self.assertRaises(RuntimeError, self.loop.run_until_complete, f) + def test__add_callback_handle(self): h = asyncio.Handle(lambda: False, (), self.loop) diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index d7fafab6917..36f65085bf5 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -80,7 +80,10 @@ class BaseSelectorEventLoopTests(unittest.TestCase): self.loop._selector.close() self.loop._selector = selector = mock.Mock() + self.assertFalse(self.loop.is_closed()) + self.loop.close() + self.assertTrue(self.loop.is_closed()) self.assertIsNone(self.loop._selector) self.assertIsNone(self.loop._csock) self.assertIsNone(self.loop._ssock) @@ -89,9 +92,20 @@ class BaseSelectorEventLoopTests(unittest.TestCase): csock.close.assert_called_with() remove_reader.assert_called_with(7) + # it should be possible to call close() more than once self.loop.close() self.loop.close() + # operation blocked when the loop is closed + f = asyncio.Future(loop=self.loop) + self.assertRaises(RuntimeError, self.loop.run_forever) + self.assertRaises(RuntimeError, self.loop.run_until_complete, f) + fd = 0 + def callback(): + pass + self.assertRaises(RuntimeError, self.loop.add_reader, fd, callback) + self.assertRaises(RuntimeError, self.loop.add_writer, fd, callback) + def test_close_no_selector(self): ssock = self.loop._ssock csock = self.loop._csock @@ -101,9 +115,6 @@ class BaseSelectorEventLoopTests(unittest.TestCase): self.loop._selector = None self.loop.close() self.assertIsNone(self.loop._selector) - self.assertFalse(ssock.close.called) - self.assertFalse(csock.close.called) - self.assertFalse(remove_reader.called) def test_socketpair(self): self.assertRaises(NotImplementedError, self.loop._socketpair) diff --git a/Misc/NEWS b/Misc/NEWS index 29f5b610644..e718bcd1211 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -22,6 +22,10 @@ Core and Builtins Library ------- +- Issue #21326: Add a new is_closed() method to asyncio.BaseEventLoop. + run_forever() and run_until_complete() methods of asyncio.BaseEventLoop now + raise an exception if the event loop was closed. + - Issue #21310: Fixed possible resource leak in failed open(). - Issue #21677: Fixed chaining nonnormalized exceptions in io close() methods.