Fix for https://github.com/python/cpython/issues/82504 This patches MultiLoopChildWatcher, which was deprecated in Python 3.12 and is scheduled for removal in Python 3.14. The patch was not accepted upstream but it seems to have worked for us and has been in our Python 3.9 without complaints. Deprecation PR: https://github.com/python/cpython/pull/98089 The patch is originally from: From: Chris Jerdonek Date: Sat, 16 May 2020 15:57:27 -0700 Subject: [PATCH 1/5] bpo-38323: Fix rare MultiLoopChildWatcher hangs. --- Python-3.12.2/Lib/asyncio/unix_events.py.orig +++ Python-3.12.2/Lib/asyncio/unix_events.py @@ -88,6 +88,8 @@ def add_signal_handler(self, sig, callback, *args): """Add a handler for a signal. UNIX only. + This method can only be called from the main thread. + Raise ValueError if the signal number is invalid or uncatchable. Raise RuntimeError if there is a problem setting up the handler. """ @@ -1253,10 +1255,15 @@ return handler = signal.getsignal(signal.SIGCHLD) - if handler != self._sig_chld: + # add_signal_handler() sets the handler to _sighandler_noop. + if handler != _sighandler_noop: logger.warning("SIGCHLD handler was changed by outside code") else: + loop = self._loop + # This clears the wakeup file descriptor if necessary. + loop.remove_signal_handler(signal.SIGCHLD) signal.signal(signal.SIGCHLD, self._saved_sighandler) + self._saved_sighandler = None def __enter__(self): @@ -1280,10 +1287,20 @@ return False def attach_loop(self, loop): + """ + This registers the SIGCHLD signal handler. + + This method can only be called from the main thread. + """ # Don't save the loop but initialize itself if called first time # The reason to do it here is that attach_loop() is called from # unix policy only for the main thread. # Main thread is required for subscription on SIGCHLD signal + if loop is None or self._saved_sighandler is not None: + return + + self._loop = loop + self._saved_sighandler = signal.getsignal(signal.SIGCHLD) if self._saved_sighandler is not None: return @@ -1293,8 +1310,14 @@ "restore to default handler on watcher close.") self._saved_sighandler = signal.SIG_DFL - # Set SA_RESTART to limit EINTR occurrences. - signal.siginterrupt(signal.SIGCHLD, False) + if self._callbacks: + warnings.warn( + 'A loop is being detached ' + 'from a child watcher with pending handlers', + RuntimeWarning) + + # This also sets up the wakeup file descriptor. + loop.add_signal_handler(signal.SIGCHLD, self._sig_chld) def _do_waitpid_all(self): for pid in list(self._callbacks): @@ -1337,7 +1360,7 @@ expected_pid, returncode) loop.call_soon_threadsafe(callback, pid, returncode, *args) - def _sig_chld(self, signum, frame): + def _sig_chld(self, *args): try: self._do_waitpid_all() except (SystemExit, KeyboardInterrupt):