Skip to content

Commit bb2fc5b

Browse files
committed
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".
1 parent 1538665 commit bb2fc5b

File tree

7 files changed

+80
-19
lines changed

7 files changed

+80
-19
lines changed

Doc/library/asyncio-eventloop.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ Run an event loop
119119
Callback scheduled after :meth:`stop` is called won't. However, those
120120
callbacks will run if :meth:`run_forever` is called again later.
121121

122+
.. method:: BaseEventLoop.is_closed()
123+
124+
Returns ``True`` if the event loop was closed.
125+
126+
.. versionadded:: 3.4.2
127+
122128
.. method:: BaseEventLoop.close()
123129

124130
Close the event loop. The loop should not be running.

Lib/asyncio/base_events.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def wait_closed(self):
119119
class BaseEventLoop(events.AbstractEventLoop):
120120

121121
def __init__(self):
122+
self._closed = False
122123
self._ready = collections.deque()
123124
self._scheduled = []
124125
self._default_executor = None
@@ -128,6 +129,11 @@ def __init__(self):
128129
self._exception_handler = None
129130
self._debug = False
130131

132+
def __repr__(self):
133+
return ('<%s running=%s closed=%s debug=%s>'
134+
% (self.__class__.__name__, self.is_running(),
135+
self.is_closed(), self.get_debug()))
136+
131137
def _make_socket_transport(self, sock, protocol, waiter=None, *,
132138
extra=None, server=None):
133139
"""Create socket transport."""
@@ -173,8 +179,13 @@ def _process_events(self, event_list):
173179
"""Process selector events."""
174180
raise NotImplementedError
175181

182+
def _check_closed(self):
183+
if self._closed:
184+
raise RuntimeError('Event loop is closed')
185+
176186
def run_forever(self):
177187
"""Run until stop() is called."""
188+
self._check_closed()
178189
if self._running:
179190
raise RuntimeError('Event loop is running.')
180191
self._running = True
@@ -198,6 +209,7 @@ def run_until_complete(self, future):
198209
199210
Return the Future's result, or raise its exception.
200211
"""
212+
self._check_closed()
201213
future = tasks.async(future, loop=self)
202214
future.add_done_callback(_raise_stop_error)
203215
self.run_forever()
@@ -222,13 +234,20 @@ def close(self):
222234
This clears the queues and shuts down the executor,
223235
but does not wait for the executor to finish.
224236
"""
237+
if self._closed:
238+
return
239+
self._closed = True
225240
self._ready.clear()
226241
self._scheduled.clear()
227242
executor = self._default_executor
228243
if executor is not None:
229244
self._default_executor = None
230245
executor.shutdown(wait=False)
231246

247+
def is_closed(self):
248+
"""Returns True if the event loop was closed."""
249+
return self._closed
250+
232251
def is_running(self):
233252
"""Returns running status of event loop."""
234253
return self._running

Lib/asyncio/proactor_events.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,14 @@ def _make_write_pipe_transport(self, sock, protocol, waiter=None,
353353
sock, protocol, waiter, extra)
354354

355355
def close(self):
356-
if self._proactor is not None:
357-
self._close_self_pipe()
358-
self._proactor.close()
359-
self._proactor = None
360-
self._selector = None
361-
super().close()
362-
self._accept_futures.clear()
356+
if self.is_closed():
357+
return
358+
self._stop_accept_futures()
359+
self._close_self_pipe()
360+
self._proactor.close()
361+
self._proactor = None
362+
self._selector = None
363+
super().close()
363364

364365
def sock_recv(self, sock, n):
365366
return self._proactor.recv(sock, n)
@@ -428,6 +429,8 @@ def loop(f=None):
428429
self._make_socket_transport(
429430
conn, protocol,
430431
extra={'peername': addr}, server=server)
432+
if self.is_closed():
433+
return
431434
f = self._proactor.accept(sock)
432435
except OSError as exc:
433436
if sock.fileno() != -1:
@@ -448,8 +451,12 @@ def loop(f=None):
448451
def _process_events(self, event_list):
449452
pass # XXX hard work currently done in poll
450453

451-
def _stop_serving(self, sock):
454+
def _stop_accept_futures(self):
452455
for future in self._accept_futures.values():
453456
future.cancel()
457+
self._accept_futures.clear()
458+
459+
def _stop_serving(self, sock):
460+
self._stop_accept_futures()
454461
self._proactor._stop_serving(sock)
455462
sock.close()

Lib/asyncio/selector_events.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ def _make_datagram_transport(self, sock, protocol,
5555
return _SelectorDatagramTransport(self, sock, protocol, address, extra)
5656

5757
def close(self):
58+
if self.is_closed():
59+
return
60+
self._close_self_pipe()
5861
if self._selector is not None:
59-
self._close_self_pipe()
6062
self._selector.close()
6163
self._selector = None
62-
super().close()
64+
super().close()
6365

6466
def _socketpair(self):
6567
raise NotImplementedError
@@ -143,8 +145,7 @@ def _accept_connection(self, protocol_factory, sock,
143145

144146
def add_reader(self, fd, callback, *args):
145147
"""Add a reader callback."""
146-
if self._selector is None:
147-
raise RuntimeError('Event loop is closed')
148+
self._check_closed()
148149
handle = events.Handle(callback, args, self)
149150
try:
150151
key = self._selector.get_key(fd)
@@ -160,7 +161,7 @@ def add_reader(self, fd, callback, *args):
160161

161162
def remove_reader(self, fd):
162163
"""Remove a reader callback."""
163-
if self._selector is None:
164+
if self.is_closed():
164165
return False
165166
try:
166167
key = self._selector.get_key(fd)
@@ -182,8 +183,7 @@ def remove_reader(self, fd):
182183

183184
def add_writer(self, fd, callback, *args):
184185
"""Add a writer callback.."""
185-
if self._selector is None:
186-
raise RuntimeError('Event loop is closed')
186+
self._check_closed()
187187
handle = events.Handle(callback, args, self)
188188
try:
189189
key = self._selector.get_key(fd)
@@ -199,7 +199,7 @@ def add_writer(self, fd, callback, *args):
199199

200200
def remove_writer(self, fd):
201201
"""Remove a writer callback."""
202-
if self._selector is None:
202+
if self.is_closed():
203203
return False
204204
try:
205205
key = self._selector.get_key(fd)

Lib/test/test_asyncio/test_base_events.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ def test_not_implemented(self):
5252
gen = self.loop._make_subprocess_transport(m, m, m, m, m, m, m)
5353
self.assertRaises(NotImplementedError, next, iter(gen))
5454

55+
def test_close(self):
56+
self.assertFalse(self.loop.is_closed())
57+
self.loop.close()
58+
self.assertTrue(self.loop.is_closed())
59+
60+
# it should be possible to call close() more than once
61+
self.loop.close()
62+
self.loop.close()
63+
64+
# operation blocked when the loop is closed
65+
f = asyncio.Future(loop=self.loop)
66+
self.assertRaises(RuntimeError, self.loop.run_forever)
67+
self.assertRaises(RuntimeError, self.loop.run_until_complete, f)
68+
5569
def test__add_callback_handle(self):
5670
h = asyncio.Handle(lambda: False, (), self.loop)
5771

Lib/test/test_asyncio/test_selector_events.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ def test_close(self):
8080

8181
self.loop._selector.close()
8282
self.loop._selector = selector = mock.Mock()
83+
self.assertFalse(self.loop.is_closed())
84+
8385
self.loop.close()
86+
self.assertTrue(self.loop.is_closed())
8487
self.assertIsNone(self.loop._selector)
8588
self.assertIsNone(self.loop._csock)
8689
self.assertIsNone(self.loop._ssock)
@@ -89,9 +92,20 @@ def test_close(self):
8992
csock.close.assert_called_with()
9093
remove_reader.assert_called_with(7)
9194

95+
# it should be possible to call close() more than once
9296
self.loop.close()
9397
self.loop.close()
9498

99+
# operation blocked when the loop is closed
100+
f = asyncio.Future(loop=self.loop)
101+
self.assertRaises(RuntimeError, self.loop.run_forever)
102+
self.assertRaises(RuntimeError, self.loop.run_until_complete, f)
103+
fd = 0
104+
def callback():
105+
pass
106+
self.assertRaises(RuntimeError, self.loop.add_reader, fd, callback)
107+
self.assertRaises(RuntimeError, self.loop.add_writer, fd, callback)
108+
95109
def test_close_no_selector(self):
96110
ssock = self.loop._ssock
97111
csock = self.loop._csock
@@ -101,9 +115,6 @@ def test_close_no_selector(self):
101115
self.loop._selector = None
102116
self.loop.close()
103117
self.assertIsNone(self.loop._selector)
104-
self.assertFalse(ssock.close.called)
105-
self.assertFalse(csock.close.called)
106-
self.assertFalse(remove_reader.called)
107118

108119
def test_socketpair(self):
109120
self.assertRaises(NotImplementedError, self.loop._socketpair)

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Core and Builtins
2222
Library
2323
-------
2424

25+
- Issue #21326: Add a new is_closed() method to asyncio.BaseEventLoop.
26+
run_forever() and run_until_complete() methods of asyncio.BaseEventLoop now
27+
raise an exception if the event loop was closed.
28+
2529
- Issue #21310: Fixed possible resource leak in failed open().
2630

2731
- Issue #21677: Fixed chaining nonnormalized exceptions in io close() methods.

0 commit comments

Comments
 (0)