Skip to content

Commit 733dac0

Browse files
GH-120804: Remove SafeChildWatcher, FastChildWatcher and MultiLoopChildWatcher from asyncio (#120805)
Remove SafeChildWatcher, FastChildWatcher and MultiLoopChildWatcher from asyncio. These child watchers have been deprecated since Python 3.12. The tests are also removed and some more tests will be added after the rewrite of child watchers.
1 parent a2f6f7d commit 733dac0

File tree

5 files changed

+4
-1041
lines changed

5 files changed

+4
-1041
lines changed

Lib/asyncio/unix_events.py

+3-322
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828

2929
__all__ = (
3030
'SelectorEventLoop',
31-
'AbstractChildWatcher', 'SafeChildWatcher',
32-
'FastChildWatcher', 'PidfdChildWatcher',
33-
'MultiLoopChildWatcher', 'ThreadedChildWatcher',
31+
'AbstractChildWatcher',
32+
'PidfdChildWatcher',
33+
'ThreadedChildWatcher',
3434
'DefaultEventLoopPolicy',
3535
'EventLoop',
3636
)
@@ -1062,325 +1062,6 @@ def _sig_chld(self):
10621062
})
10631063

10641064

1065-
class SafeChildWatcher(BaseChildWatcher):
1066-
"""'Safe' child watcher implementation.
1067-
1068-
This implementation avoids disrupting other code spawning processes by
1069-
polling explicitly each process in the SIGCHLD handler instead of calling
1070-
os.waitpid(-1).
1071-
1072-
This is a safe solution but it has a significant overhead when handling a
1073-
big number of children (O(n) each time SIGCHLD is raised)
1074-
"""
1075-
1076-
def __init__(self):
1077-
super().__init__()
1078-
warnings._deprecated("SafeChildWatcher",
1079-
"{name!r} is deprecated as of Python 3.12 and will be "
1080-
"removed in Python {remove}.",
1081-
remove=(3, 14))
1082-
1083-
def close(self):
1084-
self._callbacks.clear()
1085-
super().close()
1086-
1087-
def __enter__(self):
1088-
return self
1089-
1090-
def __exit__(self, a, b, c):
1091-
pass
1092-
1093-
def add_child_handler(self, pid, callback, *args):
1094-
self._callbacks[pid] = (callback, args)
1095-
1096-
# Prevent a race condition in case the child is already terminated.
1097-
self._do_waitpid(pid)
1098-
1099-
def remove_child_handler(self, pid):
1100-
try:
1101-
del self._callbacks[pid]
1102-
return True
1103-
except KeyError:
1104-
return False
1105-
1106-
def _do_waitpid_all(self):
1107-
1108-
for pid in list(self._callbacks):
1109-
self._do_waitpid(pid)
1110-
1111-
def _do_waitpid(self, expected_pid):
1112-
assert expected_pid > 0
1113-
1114-
try:
1115-
pid, status = os.waitpid(expected_pid, os.WNOHANG)
1116-
except ChildProcessError:
1117-
# The child process is already reaped
1118-
# (may happen if waitpid() is called elsewhere).
1119-
pid = expected_pid
1120-
returncode = 255
1121-
logger.warning(
1122-
"Unknown child process pid %d, will report returncode 255",
1123-
pid)
1124-
else:
1125-
if pid == 0:
1126-
# The child process is still alive.
1127-
return
1128-
1129-
returncode = waitstatus_to_exitcode(status)
1130-
if self._loop.get_debug():
1131-
logger.debug('process %s exited with returncode %s',
1132-
expected_pid, returncode)
1133-
1134-
try:
1135-
callback, args = self._callbacks.pop(pid)
1136-
except KeyError: # pragma: no cover
1137-
# May happen if .remove_child_handler() is called
1138-
# after os.waitpid() returns.
1139-
if self._loop.get_debug():
1140-
logger.warning("Child watcher got an unexpected pid: %r",
1141-
pid, exc_info=True)
1142-
else:
1143-
callback(pid, returncode, *args)
1144-
1145-
1146-
class FastChildWatcher(BaseChildWatcher):
1147-
"""'Fast' child watcher implementation.
1148-
1149-
This implementation reaps every terminated processes by calling
1150-
os.waitpid(-1) directly, possibly breaking other code spawning processes
1151-
and waiting for their termination.
1152-
1153-
There is no noticeable overhead when handling a big number of children
1154-
(O(1) each time a child terminates).
1155-
"""
1156-
def __init__(self):
1157-
super().__init__()
1158-
self._lock = threading.Lock()
1159-
self._zombies = {}
1160-
self._forks = 0
1161-
warnings._deprecated("FastChildWatcher",
1162-
"{name!r} is deprecated as of Python 3.12 and will be "
1163-
"removed in Python {remove}.",
1164-
remove=(3, 14))
1165-
1166-
def close(self):
1167-
self._callbacks.clear()
1168-
self._zombies.clear()
1169-
super().close()
1170-
1171-
def __enter__(self):
1172-
with self._lock:
1173-
self._forks += 1
1174-
1175-
return self
1176-
1177-
def __exit__(self, a, b, c):
1178-
with self._lock:
1179-
self._forks -= 1
1180-
1181-
if self._forks or not self._zombies:
1182-
return
1183-
1184-
collateral_victims = str(self._zombies)
1185-
self._zombies.clear()
1186-
1187-
logger.warning(
1188-
"Caught subprocesses termination from unknown pids: %s",
1189-
collateral_victims)
1190-
1191-
def add_child_handler(self, pid, callback, *args):
1192-
assert self._forks, "Must use the context manager"
1193-
1194-
with self._lock:
1195-
try:
1196-
returncode = self._zombies.pop(pid)
1197-
except KeyError:
1198-
# The child is running.
1199-
self._callbacks[pid] = callback, args
1200-
return
1201-
1202-
# The child is dead already. We can fire the callback.
1203-
callback(pid, returncode, *args)
1204-
1205-
def remove_child_handler(self, pid):
1206-
try:
1207-
del self._callbacks[pid]
1208-
return True
1209-
except KeyError:
1210-
return False
1211-
1212-
def _do_waitpid_all(self):
1213-
# Because of signal coalescing, we must keep calling waitpid() as
1214-
# long as we're able to reap a child.
1215-
while True:
1216-
try:
1217-
pid, status = os.waitpid(-1, os.WNOHANG)
1218-
except ChildProcessError:
1219-
# No more child processes exist.
1220-
return
1221-
else:
1222-
if pid == 0:
1223-
# A child process is still alive.
1224-
return
1225-
1226-
returncode = waitstatus_to_exitcode(status)
1227-
1228-
with self._lock:
1229-
try:
1230-
callback, args = self._callbacks.pop(pid)
1231-
except KeyError:
1232-
# unknown child
1233-
if self._forks:
1234-
# It may not be registered yet.
1235-
self._zombies[pid] = returncode
1236-
if self._loop.get_debug():
1237-
logger.debug('unknown process %s exited '
1238-
'with returncode %s',
1239-
pid, returncode)
1240-
continue
1241-
callback = None
1242-
else:
1243-
if self._loop.get_debug():
1244-
logger.debug('process %s exited with returncode %s',
1245-
pid, returncode)
1246-
1247-
if callback is None:
1248-
logger.warning(
1249-
"Caught subprocess termination from unknown pid: "
1250-
"%d -> %d", pid, returncode)
1251-
else:
1252-
callback(pid, returncode, *args)
1253-
1254-
1255-
class MultiLoopChildWatcher(AbstractChildWatcher):
1256-
"""A watcher that doesn't require running loop in the main thread.
1257-
1258-
This implementation registers a SIGCHLD signal handler on
1259-
instantiation (which may conflict with other code that
1260-
install own handler for this signal).
1261-
1262-
The solution is safe but it has a significant overhead when
1263-
handling a big number of processes (*O(n)* each time a
1264-
SIGCHLD is received).
1265-
"""
1266-
1267-
# Implementation note:
1268-
# The class keeps compatibility with AbstractChildWatcher ABC
1269-
# To achieve this it has empty attach_loop() method
1270-
# and doesn't accept explicit loop argument
1271-
# for add_child_handler()/remove_child_handler()
1272-
# but retrieves the current loop by get_running_loop()
1273-
1274-
def __init__(self):
1275-
self._callbacks = {}
1276-
self._saved_sighandler = None
1277-
warnings._deprecated("MultiLoopChildWatcher",
1278-
"{name!r} is deprecated as of Python 3.12 and will be "
1279-
"removed in Python {remove}.",
1280-
remove=(3, 14))
1281-
1282-
def is_active(self):
1283-
return self._saved_sighandler is not None
1284-
1285-
def close(self):
1286-
self._callbacks.clear()
1287-
if self._saved_sighandler is None:
1288-
return
1289-
1290-
handler = signal.getsignal(signal.SIGCHLD)
1291-
if handler != self._sig_chld:
1292-
logger.warning("SIGCHLD handler was changed by outside code")
1293-
else:
1294-
signal.signal(signal.SIGCHLD, self._saved_sighandler)
1295-
self._saved_sighandler = None
1296-
1297-
def __enter__(self):
1298-
return self
1299-
1300-
def __exit__(self, exc_type, exc_val, exc_tb):
1301-
pass
1302-
1303-
def add_child_handler(self, pid, callback, *args):
1304-
loop = events.get_running_loop()
1305-
self._callbacks[pid] = (loop, callback, args)
1306-
1307-
# Prevent a race condition in case the child is already terminated.
1308-
self._do_waitpid(pid)
1309-
1310-
def remove_child_handler(self, pid):
1311-
try:
1312-
del self._callbacks[pid]
1313-
return True
1314-
except KeyError:
1315-
return False
1316-
1317-
def attach_loop(self, loop):
1318-
# Don't save the loop but initialize itself if called first time
1319-
# The reason to do it here is that attach_loop() is called from
1320-
# unix policy only for the main thread.
1321-
# Main thread is required for subscription on SIGCHLD signal
1322-
if self._saved_sighandler is not None:
1323-
return
1324-
1325-
self._saved_sighandler = signal.signal(signal.SIGCHLD, self._sig_chld)
1326-
if self._saved_sighandler is None:
1327-
logger.warning("Previous SIGCHLD handler was set by non-Python code, "
1328-
"restore to default handler on watcher close.")
1329-
self._saved_sighandler = signal.SIG_DFL
1330-
1331-
# Set SA_RESTART to limit EINTR occurrences.
1332-
signal.siginterrupt(signal.SIGCHLD, False)
1333-
1334-
def _do_waitpid_all(self):
1335-
for pid in list(self._callbacks):
1336-
self._do_waitpid(pid)
1337-
1338-
def _do_waitpid(self, expected_pid):
1339-
assert expected_pid > 0
1340-
1341-
try:
1342-
pid, status = os.waitpid(expected_pid, os.WNOHANG)
1343-
except ChildProcessError:
1344-
# The child process is already reaped
1345-
# (may happen if waitpid() is called elsewhere).
1346-
pid = expected_pid
1347-
returncode = 255
1348-
logger.warning(
1349-
"Unknown child process pid %d, will report returncode 255",
1350-
pid)
1351-
debug_log = False
1352-
else:
1353-
if pid == 0:
1354-
# The child process is still alive.
1355-
return
1356-
1357-
returncode = waitstatus_to_exitcode(status)
1358-
debug_log = True
1359-
try:
1360-
loop, callback, args = self._callbacks.pop(pid)
1361-
except KeyError: # pragma: no cover
1362-
# May happen if .remove_child_handler() is called
1363-
# after os.waitpid() returns.
1364-
logger.warning("Child watcher got an unexpected pid: %r",
1365-
pid, exc_info=True)
1366-
else:
1367-
if loop.is_closed():
1368-
logger.warning("Loop %r that handles pid %r is closed", loop, pid)
1369-
else:
1370-
if debug_log and loop.get_debug():
1371-
logger.debug('process %s exited with returncode %s',
1372-
expected_pid, returncode)
1373-
loop.call_soon_threadsafe(callback, pid, returncode, *args)
1374-
1375-
def _sig_chld(self, signum, frame):
1376-
try:
1377-
self._do_waitpid_all()
1378-
except (SystemExit, KeyboardInterrupt):
1379-
raise
1380-
except BaseException:
1381-
logger.warning('Unknown exception in SIGCHLD handler', exc_info=True)
1382-
1383-
13841065
class ThreadedChildWatcher(AbstractChildWatcher):
13851066
"""Threaded child watcher implementation.
13861067

Lib/test/test_asyncio/test_events.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -2214,7 +2214,7 @@ def setUp(self):
22142214
super().setUp()
22152215
with warnings.catch_warnings():
22162216
warnings.simplefilter('ignore', DeprecationWarning)
2217-
watcher = asyncio.SafeChildWatcher()
2217+
watcher = asyncio.ThreadedChildWatcher()
22182218
watcher.attach_loop(self.loop)
22192219
asyncio.set_child_watcher(watcher)
22202220

@@ -2833,13 +2833,6 @@ def setUp(self):
28332833
self.loop = asyncio.new_event_loop()
28342834
asyncio.set_event_loop(self.loop)
28352835

2836-
if sys.platform != 'win32':
2837-
with warnings.catch_warnings():
2838-
warnings.simplefilter('ignore', DeprecationWarning)
2839-
watcher = asyncio.SafeChildWatcher()
2840-
watcher.attach_loop(self.loop)
2841-
asyncio.set_child_watcher(watcher)
2842-
28432836
def tearDown(self):
28442837
try:
28452838
if sys.platform != 'win32':

0 commit comments

Comments
 (0)