Skip to content

Commit 1776568

Browse files
gh-158903: Avoid AttributeError in BaseEventLoop.__del__ and close() during premature teardown
1 parent 2639fd6 commit 1776568

5 files changed

Lines changed: 70 additions & 31 deletions

File tree

‎Lib/asyncio/proactor_events.py‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -722,8 +722,10 @@ def close(self):
722722
# call_soon(), which is forbidden when the event loop is closed.
723723
self._stop_accept_futures()
724724
self._close_self_pipe()
725-
self._proactor.close()
726-
self._proactor = None
725+
proactor = getattr(self, '_proactor', None)
726+
if proactor is not None:
727+
proactor.close()
728+
self._proactor = None
727729
self._selector = None
728730

729731
# Close the event loop
@@ -799,14 +801,20 @@ async def _sendfile_native(self, transp, file, offset, count):
799801
transp.resume_reading()
800802

801803
def _close_self_pipe(self):
802-
if self._self_reading_future is not None:
803-
self._self_reading_future.cancel()
804+
self_reading_future = getattr(self, '_self_reading_future', None)
805+
if self_reading_future is not None:
806+
self_reading_future.cancel()
804807
self._self_reading_future = None
805-
self._ssock.close()
806-
self._ssock = None
807-
self._csock.close()
808-
self._csock = None
809-
self._internal_fds -= 1
808+
ssock = getattr(self, '_ssock', None)
809+
if ssock is not None:
810+
ssock.close()
811+
self._ssock = None
812+
csock = getattr(self, '_csock', None)
813+
if csock is not None:
814+
csock.close()
815+
self._csock = None
816+
if getattr(self, '_internal_fds', 0) > 0:
817+
self._internal_fds -= 1
810818

811819
def _make_self_pipe(self):
812820
# A self-socket, really. :-)
@@ -911,9 +919,12 @@ def _process_events(self, event_list):
911919
pass
912920

913921
def _stop_accept_futures(self):
914-
for future in self._accept_futures.values():
922+
accept_futures = getattr(self, '_accept_futures', None)
923+
if accept_futures is None:
924+
return
925+
for future in accept_futures.values():
915926
future.cancel()
916-
self._accept_futures.clear()
927+
accept_futures.clear()
917928

918929
def _stop_serving(self, sock):
919930
future = self._accept_futures.pop(sock.fileno(), None)

‎Lib/asyncio/selector_events.py‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,24 @@ def close(self):
104104
return
105105
self._close_self_pipe()
106106
super().close()
107-
if self._selector is not None:
108-
self._selector.close()
107+
selector = getattr(self, '_selector', None)
108+
if selector is not None:
109+
selector.close()
109110
self._selector = None
110111

111112
def _close_self_pipe(self):
112-
self._remove_reader(self._ssock.fileno())
113-
self._ssock.close()
114-
self._ssock = None
115-
self._csock.close()
116-
self._csock = None
117-
self._internal_fds -= 1
113+
ssock = getattr(self, '_ssock', None)
114+
if ssock is not None:
115+
if getattr(self, '_selector', None) is not None:
116+
self._remove_reader(ssock.fileno())
117+
ssock.close()
118+
self._ssock = None
119+
csock = getattr(self, '_csock', None)
120+
if csock is not None:
121+
csock.close()
122+
self._csock = None
123+
if getattr(self, '_internal_fds', 0) > 0:
124+
self._internal_fds -= 1
118125

119126
def _make_self_pipe(self):
120127
# A self-socket, really. :-)

‎Lib/asyncio/unix_events.py‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,19 @@ def __init__(self, selector=None):
7070

7171
def close(self):
7272
super().close()
73-
if not sys.is_finalizing():
74-
for sig in list(self._signal_handlers):
75-
self.remove_signal_handler(sig)
76-
else:
77-
if self._signal_handlers:
78-
warnings.warn(f"Closing the loop {self!r} "
79-
f"on interpreter shutdown "
80-
f"stage, skipping signal handlers removal",
81-
ResourceWarning,
82-
source=self)
83-
self._signal_handlers.clear()
73+
signal_handlers = getattr(self, '_signal_handlers', None)
74+
if signal_handlers is not None:
75+
if not sys.is_finalizing():
76+
for sig in list(signal_handlers):
77+
self.remove_signal_handler(sig)
78+
else:
79+
if signal_handlers:
80+
warnings.warn(f"Closing the loop {self!r} "
81+
f"on interpreter shutdown "
82+
f"stage, skipping signal handlers removal",
83+
ResourceWarning,
84+
source=self)
85+
signal_handlers.clear()
8486

8587
def _process_self_data(self, data):
8688
for signum in data:

‎Lib/test/test_asyncio/test_base_events.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,29 @@ def test_close(self):
217217
self.loop.close()
218218
self.loop.close()
219219

220-
# operation blocked when the loop is closed
221220
f = self.loop.create_future()
222221
self.assertRaises(RuntimeError, self.loop.run_forever)
223222
self.assertRaises(RuntimeError, self.loop.run_until_complete, f)
224223

224+
def test_close_partially_initialized_loop(self):
225+
# gh-158903: An event loop interrupted before socket setup finishes
226+
# must close cleanly and be garbage collected without AttributeError.
227+
class IncompleteSelectorLoop(asyncio.SelectorEventLoop):
228+
def __init__(self):
229+
super(asyncio.base_events.BaseEventLoop, self).__init__()
230+
231+
loop = IncompleteSelectorLoop.__new__(IncompleteSelectorLoop)
232+
asyncio.base_events.BaseEventLoop.__init__(loop)
233+
234+
# Must not raise AttributeError
235+
loop.close()
236+
237+
# Must not trigger unraisable exception during deallocation
238+
with support.catch_unraisable_exception() as cm:
239+
del loop
240+
support.gc_collect()
241+
self.assertIsNone(cm.unraisable)
242+
225243
def test__add_callback_handle(self):
226244
h = asyncio.Handle(lambda: False, (), self.loop, None)
227245

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :exc:`AttributeError` in :meth:`asyncio.BaseEventLoop.__del__` and :meth:`close` when tearing down an event loop that was interrupted during initialization.

0 commit comments

Comments
 (0)