test_forkserver_sigkill failed once during a full ./python.exe -m test -j0 run on my machine (macOS 27.0.1, Apple M3, pydebug build of main at 1705e51). The load average was around 65 at the time. It passes when run on its own.
test test.test_multiprocessing_forkserver.test_processes failed -- Traceback (most recent call last):
File "Lib/test/_test_multiprocessing.py", line 1007, in test_forkserver_sigkill
self.check_forkserver_death(signal.SIGKILL)
File "Lib/test/_test_multiprocessing.py", line 997, in check_forkserver_death
self.assertTrue(evt.is_set())
AssertionError: False is not true
(paths shortened)
This is the same failure as gh-78221 (bpo-34040). It was worked around in 2018 by shortening the child's delay relative to the parent's sleep (07888e1), but the race is still there.
check_forkserver_death() kills the forkserver right after starting the first process. After a SIGKILL, the forkserver can no longer report that child's exit status: proc.join() gets EOF on the status pipe, and Popen.poll() in Lib/multiprocessing/popen_forkserver.py sets returncode to 255 immediately. The child may still be running at that point. The test then checks evt.is_set(), which only passes if the child has set the event by then (within the 1 second sleep plus the time proc2 takes). The next line already accepts exit code 255 for the case where the forkserver was killed.
SIGINT is not affected: the forkserver ignores it, stays alive and reports the real exit code.
To reproduce it reliably, stop the first child with SIGSTOP for a few seconds after killing the forkserver:
Reproducer
importmultiprocessingasmp, os, signal, threading, timefrommultiprocessing.forkserverimport_forkserverdefsleep_and_set(evt, delay=0.0):
time.sleep(delay)
evt.set()
if__name__=='__main__':
mp.set_start_method('forkserver')
_forkserver.ensure_running()
evt=mp.Event()
proc=mp.Process(target=sleep_and_set, args=(evt, 0.5))
proc.start()
os.kill(_forkserver._forkserver_pid, signal.SIGKILL)
# Simulate the child not getting CPU time for 3 seconds.os.kill(proc.pid, signal.SIGSTOP)
threading.Timer(3.0, os.kill, (proc.pid, signal.SIGCONT)).start()
time.sleep(1.0)
proc.join()
print(proc.exitcode, evt.is_set()) # 255 Falseprint(evt.wait(60)) # True: the child did run, later
Waiting for the event with a timeout, instead of checking it right after join(), fixes it.
Activity
gh-160295: Fix race in test_forkserver_sigkill (GH-160296)