Repository navigation
bpo-33613, test_semaphore_tracker_sigint: fix race condition #7850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
5f1df08
4f9babb
26d81a3
a7d5c59
a735d49
9460da4
fb136d0
f3d0f0b
f5e5cf9
4393021
d501f2f
9e0243e
26e077b
a50912b
bcbb942
0b46f07
66ef7dc
f7d2d4d
4a09fde
c9f96c1
c0a088c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| # the next reboot. Without this semaphore tracker process, "killall | ||
| # python" would probably leave unlinked semaphores. | ||
| # | ||
|
|
||
| import os | ||
| import signal | ||
| import sys | ||
|
|
@@ -21,6 +22,8 @@ | |
| from . import util | ||
|
|
||
| __all__ = ['ensure_running', 'register', 'unregister'] | ||
| _HAVE_SIGMASK = hasattr(signal, 'pthread_sigmask') | ||
| _IGNORED_SIGNALS = (signal.SIGINT, signal.SIGTERM) | ||
|
|
||
|
|
||
| class SemaphoreTracker(object): | ||
|
|
@@ -59,15 +62,19 @@ def ensure_running(self): | |
| fds_to_pass.append(sys.stderr.fileno()) | ||
| except Exception: | ||
| pass | ||
| cmd = 'from multiprocessing.semaphore_tracker import main;main({})' | ||
| cmd = 'from multiprocessing.semaphore_tracker import main;main(%d)' | ||
| r, w = os.pipe() | ||
| try: | ||
| fds_to_pass.append(r) | ||
| # process will out live us, so no need to wait on pid | ||
| exe = spawn.get_executable() | ||
| args = [exe] + util._args_from_interpreter_flags() | ||
| args += ['-c', cmd.format(r)] | ||
| args += ['-c', cmd % r] | ||
| if _HAVE_SIGMASK: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A comment is needed to explain why you block signals. Add a reference to the bpo and explain the race condition.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hum. I would prefer to put the pthread_sigmask() inside the try block, to make sure that we always unblock, even if the code is interrupted between pthread_sigmask() and the start of the try block. Unblocking a signal which isn't blocked does nothing and so is safe.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f7d2d4d |
||
| signal.pthread_sigmask(signal.SIG_BLOCK, _IGNORED_SIGNALS) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit uneasy with this. What if
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The best would be to use posix_spawn() which supports to set the signal mask when spawning a new child process. But posix_spawn() API is still changing (PR #6693) and it's not available on all platforms. Maybe we can implement the sigmask inside spawnv_passfds(), to prepare the code to be able to use posix_spawn() later? |
||
| pid = util.spawnv_passfds(exe, args, fds_to_pass) | ||
| if _HAVE_SIGMASK: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, right. That's my fault, I proposed the code to @pablogsal :-) |
||
| signal.pthread_sigmask(signal.SIG_UNBLOCK, _IGNORED_SIGNALS) | ||
| except: | ||
| os.close(w) | ||
| raise | ||
|
|
@@ -103,12 +110,13 @@ def _send(self, cmd, name): | |
| unregister = _semaphore_tracker.unregister | ||
| getfd = _semaphore_tracker.getfd | ||
|
|
||
|
|
||
| def main(fd): | ||
| '''Run semaphore tracker.''' | ||
| # protect the process from ^C and "killall python" etc | ||
| signal.signal(signal.SIGINT, signal.SIG_IGN) | ||
| signal.signal(signal.SIGTERM, signal.SIG_IGN) | ||
| if _HAVE_SIGMASK: | ||
| signal.pthread_sigmask(signal.SIG_UNBLOCK, _IGNORED_SIGNALS) | ||
|
|
||
| for f in (sys.stdin, sys.stdout): | ||
| try: | ||
|
|
@@ -127,9 +135,6 @@ def main(fd): | |
| cache.add(name) | ||
| elif cmd == b'UNREGISTER': | ||
| cache.remove(name) | ||
| elif cmd == b'PING': | ||
| with open(sys.stderr.fileno(), "wb") as output: | ||
| output.write(b"PONG\n") | ||
| else: | ||
| raise RuntimeError('unrecognized command %r' % cmd) | ||
| except Exception: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4477,38 +4477,12 @@ def check_semaphore_tracker_death(self, signum, should_die): | |
| if pid: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the multiprocessing code uses "pid is not None". Technically, I'm not sure that it's possible to have a pid of 0, since pid 0 has a special meaning in many C functions.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm now confused, you modified "if not pid" in the other file, whereas it seems like os.waitpid() doesn't return None.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I did not understand correctly your suggestion. For some reason, I inferred that with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 822516a1403d4eaec9ac743861336f0902d8186e |
||
| os.kill(pid, signal.SIGKILL) | ||
| time.sleep(0.5) # give it time to die | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to wait until the process completes instead of using a sleep?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sadly no AFAIK, because the tracker relies on calling
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can make the child capture
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not using os.waitpid() in the test, and modify ensure_running() to handle ChildProcessError? The purpose of the waitpid() call in ensure_running() is to check if the process died. We don't care of the exit status. Adding time.sleep(0.5) means adding a new race condition, while fixing another, no? |
||
| testfn = test.support.TESTFN | ||
| self.addCleanup(test.support.unlink, testfn) | ||
| tracker_stderr_r = open(testfn, "wb") | ||
| old_stderr = os.dup(sys.stderr.fileno()) | ||
| os.dup2(tracker_stderr_r.fileno(), sys.stderr.fileno()) | ||
| try: | ||
| with warnings.catch_warnings(record=True) as all_warn: | ||
| _semaphore_tracker.ensure_running() | ||
| pid = _semaphore_tracker._pid | ||
| # Wait until we receive the PONG from the child, indicating that | ||
| # the signal handlers have been registered. See bpo-33613 for more | ||
| # information. | ||
| _semaphore_tracker._send("PING", "") | ||
| deadline = time.monotonic() + 5 | ||
| with open(testfn, "rb") as tracker_stderr: | ||
| while True: | ||
| if time.monotonic() >= deadline: | ||
| raise TimeoutError("Reading data " | ||
| "from tracker stderr took too long") | ||
| data = tracker_stderr.readline() | ||
| if not data: | ||
| continue | ||
| if b"PONG" not in data: | ||
| raise ValueError("Invalid data in stderr: {}!".format(data)) | ||
| break | ||
| finally: | ||
| tracker_stderr_r.close() | ||
| os.dup2(sys.stderr.fileno(), old_stderr) | ||
| os.close(old_stderr) | ||
| with warnings.catch_warnings(record=True) as all_warn: | ||
| _semaphore_tracker.ensure_running() | ||
| pid = _semaphore_tracker._pid | ||
|
|
||
| os.kill(pid, signum) | ||
| time.sleep(1.0) # give it time to die | ||
| time.sleep(0.5) # give it time to die | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you reduce time?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because one second seems a lot to me for the signal to be delivered (checked in the buildbot) and this will make the test suite run a bit faster. I can undo this if we want to be conservative :).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please keep 1 second. While you saw the test passing, I'm sure that reducing the sleep will make the test failure more likely on some very busy buildbots. |
||
| ctx = multiprocessing.get_context("spawn") | ||
| with warnings.catch_warnings(record=True) as all_warn: | ||
|
|
@@ -4534,6 +4508,10 @@ def test_semaphore_tracker_sigint(self): | |
| # Catchable signal (ignored by semaphore tracker) | ||
| self.check_semaphore_tracker_death(signal.SIGINT, False) | ||
|
|
||
| def test_semaphore_tracker_sigterm(self): | ||
| # Catchable signal (ignored by semaphore tracker) | ||
| self.check_semaphore_tracker_death(signal.SIGTERM, False) | ||
|
|
||
| def test_semaphore_tracker_sigkill(self): | ||
| # Uncatchable signal. | ||
| self.check_semaphore_tracker_death(signal.SIGKILL, True) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an empty line before this?