summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Peterson <benjamin@python.org>2019-11-14 03:08:50 (GMT)
committerGitHub <noreply@github.com>2019-11-14 03:08:50 (GMT)
commit3ccdd9b180f9a3f29c8ddc8ad1b331fe8df26519 (patch)
tree9353e254fbb988e8325a99269dee89af50cbd74f
parentdad6be5ffe48beb74fad78cf758b886afddc7aed (diff)
downloadcpython-3ccdd9b180f9a3f29c8ddc8ad1b331fe8df26519.zip
cpython-3ccdd9b180f9a3f29c8ddc8ad1b331fe8df26519.tar.gz
cpython-3ccdd9b180f9a3f29c8ddc8ad1b331fe8df26519.tar.bz2
closes bpo-38692: Add a pidfd child process watcher to asyncio. (GH-17069)
-rw-r--r--Doc/library/asyncio-policy.rst12
-rw-r--r--Doc/whatsnew/3.9.rst3
-rw-r--r--Lib/asyncio/unix_events.py67
-rw-r--r--Lib/test/test_asyncio/test_subprocess.py18
-rw-r--r--Misc/NEWS.d/next/Library/2019-11-05-19-15-57.bpo-38692.2DCDA-.rst2
5 files changed, 102 insertions, 0 deletions
diff --git a/Doc/library/asyncio-policy.rst b/Doc/library/asyncio-policy.rst
index aa8f8f1..d9d3232 100644
--- a/Doc/library/asyncio-policy.rst
+++ b/Doc/library/asyncio-policy.rst
@@ -257,6 +257,18 @@ implementation used by the asyncio event loop:
This solution requires a running event loop in the main thread to work, as
:class:`SafeChildWatcher`.
+.. class:: PidfdChildWatcher
+
+ This implementation polls process file descriptors (pidfds) to await child
+ process termination. In some respects, :class:`PidfdChildWatcher` is a
+ "Goldilocks" child watcher implementation. It doesn't require signals or
+ threads, doesn't interfere with any processes launched outside the event
+ loop, and scales linearly with the number of subprocesses launched by the
+ event loop. The main disadvantage is that pidfds are specific to Linux, and
+ only work on recent (5.3+) kernels.
+
+ .. versionadded:: 3.9
+
Custom Policies
===============
diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst
index 1cd21c6..b1beb0b 100644
--- a/Doc/whatsnew/3.9.rst
+++ b/Doc/whatsnew/3.9.rst
@@ -130,6 +130,9 @@ that schedules a shutdown for the default executor that waits on the
:func:`asyncio.run` has been updated to use the new :term:`coroutine`.
(Contributed by Kyle Stanley in :issue:`34037`.)
+Added :class:`asyncio.PidfdChildWatcher`, a Linux-specific child watcher
+implementation that polls process file descriptors. (:issue:`38692`)
+
curses
------
diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py
index d8f6530..d02460c 100644
--- a/Lib/asyncio/unix_events.py
+++ b/Lib/asyncio/unix_events.py
@@ -878,6 +878,73 @@ class AbstractChildWatcher:
raise NotImplementedError()
+class PidfdChildWatcher(AbstractChildWatcher):
+ """Child watcher implementation using Linux's pid file descriptors.
+
+ This child watcher polls process file descriptors (pidfds) to await child
+ process termination. In some respects, PidfdChildWatcher is a "Goldilocks"
+ child watcher implementation. It doesn't require signals or threads, doesn't
+ interfere with any processes launched outside the event loop, and scales
+ linearly with the number of subprocesses launched by the event loop. The
+ main disadvantage is that pidfds are specific to Linux, and only work on
+ recent (5.3+) kernels.
+ """
+
+ def __init__(self):
+ self._loop = None
+ self._callbacks = {}
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_traceback):
+ pass
+
+ def is_active(self):
+ return self._loop is not None and self._loop.is_running()
+
+ def close(self):
+ self.attach_loop(None)
+
+ def attach_loop(self, loop):
+ if self._loop is not None and loop is None and self._callbacks:
+ warnings.warn(
+ 'A loop is being detached '
+ 'from a child watcher with pending handlers',
+ RuntimeWarning)
+ for pidfd, _, _ in self._callbacks.values():
+ self._loop._remove_reader(pidfd)
+ os.close(pidfd)
+ self._callbacks.clear()
+ self._loop = loop
+
+ def add_child_handler(self, pid, callback, *args):
+ existing = self._callbacks.get(pid)
+ if existing is not None:
+ self._callbacks[pid] = existing[0], callback, args
+ else:
+ pidfd = os.pidfd_open(pid)
+ self._loop._add_reader(pidfd, self._do_wait, pid)
+ self._callbacks[pid] = pidfd, callback, args
+
+ def _do_wait(self, pid):
+ pidfd, callback, args = self._callbacks.pop(pid)
+ self._loop._remove_reader(pidfd)
+ _, status = os.waitpid(pid, 0)
+ os.close(pidfd)
+ returncode = _compute_returncode(status)
+ callback(pid, returncode, *args)
+
+ def remove_child_handler(self, pid):
+ try:
+ pidfd, _, _ = self._callbacks.pop(pid)
+ except KeyError:
+ return False
+ self._loop._remove_reader(pidfd)
+ os.close(pidfd)
+ return True
+
+
def _compute_returncode(status):
if os.WIFSIGNALED(status):
# The child process died because of a signal.
diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py
index 17552d0..a6c3acc 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -1,3 +1,4 @@
+import os
import signal
import sys
import unittest
@@ -691,6 +692,23 @@ if sys.platform != 'win32':
Watcher = unix_events.FastChildWatcher
+ def has_pidfd_support():
+ if not hasattr(os, 'pidfd_open'):
+ return False
+ try:
+ os.close(os.pidfd_open(os.getpid()))
+ except OSError:
+ return False
+ return True
+
+ @unittest.skipUnless(
+ has_pidfd_support(),
+ "operating system does not support pidfds",
+ )
+ class SubprocessPidfdWatcherTests(SubprocessWatcherMixin,
+ test_utils.TestCase):
+ Watcher = unix_events.PidfdChildWatcher
+
else:
# Windows
class SubprocessProactorTests(SubprocessMixin, test_utils.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2019-11-05-19-15-57.bpo-38692.2DCDA-.rst b/Misc/NEWS.d/next/Library/2019-11-05-19-15-57.bpo-38692.2DCDA-.rst
new file mode 100644
index 0000000..7c8b3e8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-11-05-19-15-57.bpo-38692.2DCDA-.rst
@@ -0,0 +1,2 @@
+Add :class:`asyncio.PidfdChildWatcher`, a Linux-specific child watcher
+implementation that polls process file descriptors.