Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5f1df08
bpo-33613, test_semaphore_tracker_sigint: fix race condition
pablogsal Jun 20, 2018
4f9babb
Implement PONG command in the semaphore tracker
pablogsal Jun 24, 2018
26d81a3
Ensure the tracker is killed to allow multiple testing runs in the sa…
pablogsal Jun 24, 2018
a7d5c59
fixup! Ensure the tracker is killed to allow multiple testing runs in…
pablogsal Jul 1, 2018
a735d49
fixup! fixup! Ensure the tracker is killed to allow multiple testing …
pablogsal Jul 7, 2018
9460da4
fixup! fixup! fixup! Ensure the tracker is killed to allow multiple t…
pablogsal Jul 24, 2018
fb136d0
Use a buffered writer to answer to the PING command
pablogsal Jul 24, 2018
f3d0f0b
Use the tracker own stderr for answering the PING command
pablogsal Jul 24, 2018
f5e5cf9
Use pthread_sigmask to avoid the race condition
pablogsal Jul 27, 2018
4393021
Add comment explaining the race condition
pablogsal Jul 27, 2018
d501f2f
Restore timeout and use os.waitpid
pablogsal Jul 31, 2018
9e0243e
fixup! Restore timeout and use os.waitpid
pablogsal Jul 31, 2018
26e077b
Add News entry
pablogsal Jul 31, 2018
a50912b
fixup! fixup! Restore timeout and use os.waitpid
pablogsal Jul 31, 2018
bcbb942
fixup! Add News entry
pablogsal Jul 31, 2018
0b46f07
fixup! fixup! fixup! Restore timeout and use os.waitpid
pablogsal Aug 1, 2018
66ef7dc
Unregister sigmask if process fails to start
pablogsal Aug 3, 2018
f7d2d4d
Include the signal registering in the try block
pablogsal Sep 3, 2018
4a09fde
Simplify comment when handling ChildProcessError
pablogsal Sep 3, 2018
c9f96c1
Eliminate hardcoded reference to the bug tracker url
pablogsal Sep 3, 2018
c0a088c
Remove extraneous NEWS file
pitrou Sep 4, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use pthread_sigmask to avoid the race condition
  • Loading branch information
pablogsal committed Jul 27, 2018
commit f5e5cf904e640997dbb3bf5d7eaef3d7e850e790
17 changes: 11 additions & 6 deletions Lib/multiprocessing/semaphore_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,8 @@
from . import util

__all__ = ['ensure_running', 'register', 'unregister']
_HAVE_SIGMASK = hasattr(signal, 'pthread_sigmask')

Copy link
Copy Markdown
Member

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?

_IGNORED_SIGNALS = (signal.SIGINT, signal.SIGTERM)


class SemaphoreTracker(object):
Expand Down Expand Up @@ -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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f7d2d4d

signal.pthread_sigmask(signal.SIG_BLOCK, _IGNORED_SIGNALS)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uneasy with this. What if spawnv_passfds takes a very long time for some reason? The user will try to stop it using ^C... and nothing will happen.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use a try..finally block.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
38 changes: 8 additions & 30 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4477,38 +4477,12 @@ def check_semaphore_tracker_death(self, signum, should_die):
if pid:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use pid is not None here. I think is clearer what the value you are expecting not to be.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

@pablogsal pablogsal Aug 1, 2018 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 WNOHANG, waitpid could return None (as waitid can do if you provide WNOHANG). I think I understand now. Mea culpa :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly no AFAIK, because the tracker relies on calling waitpid to check the child. If you add a os.waitpid here you will get:


ERROR: test_semaphore_tracker_sigint (test.test_multiprocessing_fork.TestSemaphoreTracker)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/pgalindo3/cpython/Lib/test/_test_multiprocessing.py", line 4509, in test_semaphore_tracker_sigint
    self.check_semaphore_tracker_death(signal.SIGINT, False)
  File "/Users/pgalindo3/cpython/Lib/test/_test_multiprocessing.py", line 4481, in check_semaphore_tracker_death
    _semaphore_tracker.ensure_running()
  File "/Users/pgalindo3/cpython/Lib/multiprocessing/semaphore_tracker.py", line 48, in ensure_running
    pid, status = os.waitpid(self._pid, os.WNOHANG)
ChildProcessError: [Errno 10] No child processes
Ran 1 test in 0.068s

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make the child capture ChildProcessError on that waitpid and treat that as if the child has died, making it possible to call waitpid in the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you reduce time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 :).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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:
Expand All @@ -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)
Expand Down