Open gschaffner opened 10 months ago
Attempting any sort of event loop operation in a destructor is hazardous because you can't be sure which thread the destructor runs on. On CPython you can usually ensure that it happens in the event loop's thread, but if a different GC implementation is used (PyPy et al), this could happen in any thread.
I am now seeing this as well (not always tho) after upgrading to 3.12.3 (from 3.11.?) on Arch when KeyboardInterrupt
ing asyncio.run()
:
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7dc21d030d60>
Traceback (most recent call last):
File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 126, in __del__
self.close()
File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 104, in close
proto.pipe.close()
File "/usr/lib/python3.12/asyncio/unix_events.py", line 767, in close
self.write_eof()
File "/usr/lib/python3.12/asyncio/unix_events.py", line 753, in write_eof
self._loop.call_soon(self._call_connection_lost, None)
File "/usr/lib/python3.12/asyncio/base_events.py", line 795, in call_soon
self._check_closed()
File "/usr/lib/python3.12/asyncio/base_events.py", line 541, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Bug report
Bug description:
there is a race where it's possible for
BaseSubprocessTransport.__del__
to try to close the transport after the event loop has been closed. this results in an unraisable exception in__del__
, and it can also result in an orphan process being leaked.the following is a reproducer that triggers the race between
run()
exiting and [the process dying and the event loop learning about the process's death]. on my machine, with this reproducer the bug occurs (due torun()
winning the race) maybe 90% of the time:most of the time, running this emits
this case looks similar to GH-109538. i think the following patch (analogous to GH-111983) fixes it:
however, there is another case for which the above patch is not sufficient. in the above example the user orphaned the process after sending
SIGKILL
/TerminateProcess
(which is not immediate, but only schedules the kill), but what if they fully orphan it?currently (on
main
), when the race condition occurs (for this example the condition isrun()
winning the race againstBaseSubprocessTransport
GC) then asyncio emits a loud complaintException ignored in: <function BaseSubprocessTransport.__del__ at 0x7f5b3b291e40>
and leaks the orphan process (checkhtop
after the interpreter exits!). asyncio probably also leaks the pipes.but with the patch above, asyncio will quietly leak the orphan process (and probably pipes), but it will not yell about the leak unless the user enables
ResourceWarning
s. which is not good.so a more correct patch (fixes both cases) may be something along the lines of
with this patch applied, neither example leaks an orphan process out of
run()
, and both examples emitResourceWarning
. however this patch is rather messy. it is also perhaps still leaking pipe fd's out ofrun()
. (the fd's probably get closed by the OS when the interpreter shuts down, but i suspect one end of each pipe will be an orphan from the time whenrun()
exits to the time when the interpreter shuts down, which can be arbitrarily long).CPython versions tested on:
3.11, CPython main branch
Operating systems tested on:
Linux