8000 [3.6] bpo-31308: If multiprocessing's forkserver dies, launch it agai… · python/cpython@019c99f · GitHub
[go: up one dir, main page]

Skip to content

Commit 019c99f

Browse files
authored
[3.6] bpo-31308: If multiprocessing's forkserver dies, launch it again when necessary (GH-3246) (#4252)
* bpo-31308: If multiprocessing's forkserver dies, launch it again when necessary. * Fix test on Windows * Add NEWS entry * Adopt a different approach: ignore SIGINT and SIGTERM, as in semaphore tracker. * Fix comment * Make sure the test doesn't muck with process state * Also test previously-started processes * Update 2017-08-30-17-59-36.bpo-31308.KbexyC.rst * Avoid masking SIGTERM in forkserver. It's not necessary and causes a race condition in test_many_processes.. (cherry picked from commit fc6b348)
1 parent 5fbe5e1 commit 019c99f

File tree

3 files changed

+66
-5
lines changed

3 files changed

+66
-5
lines changed

Lib/multiprocessing/forkserver.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ForkServer(object):
3333
def __init__(self):
3434
self._forkserver_address = None
3535
self._forkserver_alive_fd = None
36+
self._forkserver_pid = None
3637
self._inherited_fds = None
3738
self._lock = threading.Lock()
3839
self._preload_modules = ['__main__']
@@ -89,8 +90,17 @@ def ensure_running(self):
8990
'''
9091
with self._lock:
9192
semaphore_tracker.ensure_running()
92-
if self._forkserver_alive_fd is not None:
93-
return
93+
if self._forkserver_pid is not None:
94+
# forkserver was launched before, is it still running?
95+
pid, status = os.waitpid(self._forkserver_pid, os.WNOHANG)
96+
if not pid:
97+
# still alive
98+
return
99+
# dead, launch it again
100+
os.close(self._forkserver_alive_fd)
101+
self._forkserver_address = None
102+
self._forkserver_alive_fd = None
103+
self._forkserver_pid = None
94104

95105
cmd = ('from multiprocessing.forkserver import main; ' +
96106
'main(%d, %d, %r, **%r)')
@@ -127,6 +137,7 @@ def ensure_running(self):
127137
os.close(alive_r)
128138
self._forkserver_address = address
129139
self._forkserver_alive_fd = alive_w
140+
self._forkserver_pid = pid
130141

131142
#
132143
#
@@ -149,11 +160,11 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None):
149160

150161
util._close_stdin()
151162

152-
# ignoring SIGCHLD means no need to reap zombie processes;
153-
# letting SIGINT through avoids KeyboardInterrupt tracebacks
154163
handlers = {
164+
# no need to reap zombie processes;
155165
signal.SIGCHLD: signal.SIG_IGN,
156-
signal.SIGINT: signal.SIG_DFL,
166+
# protect the process from ^C
167+
signal.SIGINT: signal.SIG_IGN,
157168
}
158169
old_handlers = {sig: signal.signal(sig, val)
159170
for (sig, val) in handlers.items()}

Lib/test/_test_multiprocessing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,54 @@ def test_error_on_stdio_flush(self):
446446
finally:
447447
setattr(sys, stream_name, old_stream)
448448

449+
@classmethod
450+
def _sleep_and_set_event(self, evt, delay=0.0):
451+
time.sleep(delay)
452+
evt.set()
453+
454+
def check_forkserver_death(self, signum):
455+
# bpo-31308: if the forkserver process has died, we should still
456+
# be able to create and run new Process instances (the forkserver
457+
# is implicitly restarted).
458+
if self.TYPE == 'threads':
459+
self.skipTest('test not appropriate for {}'.format(self.TYPE))
460+
sm = multiprocessing.get_start_method()
461+
if sm != 'forkserver':
462+
# The fork method by design inherits all fds from the parent,
463+
# trying to go against it is a lost battle
464+
self.skipTest('test not appropriate for {}'.format(sm))
465+
466+
from multiprocessing.forkserver import _forkserver
467+
_forkserver.ensure_running()
468+
469+
evt = self.Event()
470+
proc = self.Process(target=self._sleep_and_set_event, args=(evt, 1.0))
471+
proc.start()
4 8000 72+
473+
pid = _forkserver._forkserver_pid
474+
os.kill(pid, signum)
475+
time.sleep(1.0) # give it time to die
476+
477+
evt2 = self.Event()
478+
proc2 = self.Process(target=self._sleep_and_set_event, args=(evt2,))
479+
proc2.start()
480+
proc2.join()
481+
self.assertTrue(evt2.is_set())
482+
self.assertEqual(proc2.exitcode, 0)
483+
484+
proc.join()
485+
self.assertTrue(evt.is_set())
486+
self.assertIn(proc.exitcode, (0, 255))
487+
488+
def test_forkserver_sigint(self):
489+
# Catchable signal
490+
self.check_forkserver_death(signal.SIGINT)
491+
492+
def test_forkserver_sigkill(self):
493+
# Uncatchable signal
494+
if os.name != 'nt':
495+
self.check_forkserver_death(signal.SIGKILL)
496+
449497

450498
#
451499
#
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make multiprocessing's forkserver process immune to Ctrl-C and other user interruptions.
2+
If it crashes, restart it when necessary.

0 commit comments

Comments
 (0)
0