Issue #23309: Avoid a deadlock at shutdown if a daemon thread is aborted

while it is holding a lock to a buffered I/O object, and the main thread
tries to use the same I/O object (typically stdout or stderr).  A fatal
error is emitted instead.
This commit is contained in:
Antoine Pitrou 2015-04-13 19:48:19 +02:00
commit cb46f0ecb0
5 changed files with 94 additions and 14 deletions

View File

@ -1,6 +1,7 @@
# Common utility functions used by various script execution tests # Common utility functions used by various script execution tests
# e.g. test_cmd_line, test_cmd_line_script and test_runpy # e.g. test_cmd_line, test_cmd_line_script and test_runpy
import collections
import importlib import importlib
import sys import sys
import os import os
@ -50,8 +51,12 @@ def interpreter_requires_environment():
return __cached_interp_requires_environment return __cached_interp_requires_environment
_PythonRunResult = collections.namedtuple("_PythonRunResult",
("rc", "out", "err"))
# Executing the interpreter in a subprocess # Executing the interpreter in a subprocess
def _assert_python(expected_success, *args, **env_vars): def run_python_until_end(*args, **env_vars):
env_required = interpreter_requires_environment() env_required = interpreter_requires_environment()
if '__isolated' in env_vars: if '__isolated' in env_vars:
isolated = env_vars.pop('__isolated') isolated = env_vars.pop('__isolated')
@ -85,9 +90,14 @@ def _assert_python(expected_success, *args, **env_vars):
p.stderr.close() p.stderr.close()
rc = p.returncode rc = p.returncode
err = strip_python_stderr(err) err = strip_python_stderr(err)
if (rc and expected_success) or (not rc and not expected_success): return _PythonRunResult(rc, out, err), cmd_line
def _assert_python(expected_success, *args, **env_vars):
res, cmd_line = run_python_until_end(*args, **env_vars)
if (res.rc and expected_success) or (not res.rc and not expected_success):
# Limit to 80 lines to ASCII characters # Limit to 80 lines to ASCII characters
maxlen = 80 * 100 maxlen = 80 * 100
out, err = res.out, res.err
if len(out) > maxlen: if len(out) > maxlen:
out = b'(... truncated stdout ...)' + out[-maxlen:] out = b'(... truncated stdout ...)' + out[-maxlen:]
if len(err) > maxlen: if len(err) > maxlen:
@ -106,10 +116,10 @@ def _assert_python(expected_success, *args, **env_vars):
"---\n" "---\n"
"%s\n" "%s\n"
"---" "---"
% (rc, cmd_line, % (res.rc, cmd_line,
out, out,
err)) err))
return rc, out, err return res
def assert_python_ok(*args, **env_vars): def assert_python_ok(*args, **env_vars):
""" """

View File

@ -35,7 +35,7 @@ import weakref
from collections import deque, UserList from collections import deque, UserList
from itertools import cycle, count from itertools import cycle, count
from test import support from test import support
from test.script_helper import assert_python_ok from test.script_helper import assert_python_ok, run_python_until_end
import codecs import codecs
import io # C implementation of io import io # C implementation of io
@ -3437,6 +3437,49 @@ class CMiscIOTest(MiscIOTest):
b = bytearray(2) b = bytearray(2)
self.assertRaises(ValueError, bufio.readinto, b) self.assertRaises(ValueError, bufio.readinto, b)
@unittest.skipUnless(threading, 'Threading required for this test.')
def check_daemon_threads_shutdown_deadlock(self, stream_name):
# Issue #23309: deadlocks at shutdown should be avoided when a
# daemon thread and the main thread both write to a file.
code = """if 1:
import sys
import time
import threading
file = sys.{stream_name}
def run():
while True:
file.write('.')
file.flush()
thread = threading.Thread(target=run)
thread.daemon = True
thread.start()
time.sleep(0.5)
file.write('!')
file.flush()
""".format_map(locals())
res, _ = run_python_until_end("-c", code)
err = res.err.decode()
if res.rc != 0:
# Failure: should be a fatal error
self.assertIn("Fatal Python error: could not acquire lock "
"for <_io.BufferedWriter name='<{stream_name}>'> "
"at interpreter shutdown, possibly due to "
"daemon threads".format_map(locals()),
err)
else:
self.assertFalse(err.strip('.!'))
def test_daemon_threads_shutdown_stdout_deadlock(self):
self.check_daemon_threads_shutdown_deadlock('stdout')
def test_daemon_threads_shutdown_stderr_deadlock(self):
self.check_daemon_threads_shutdown_deadlock('stderr')
class PyMiscIOTest(MiscIOTest): class PyMiscIOTest(MiscIOTest):
io = pyio io = pyio

View File

@ -8,26 +8,27 @@ from unittest import mock
class TestScriptHelper(unittest.TestCase): class TestScriptHelper(unittest.TestCase):
def test_assert_python_expect_success(self):
t = script_helper._assert_python(True, '-c', 'import sys; sys.exit(0)') def test_assert_python_ok(self):
t = script_helper.assert_python_ok('-c', 'import sys; sys.exit(0)')
self.assertEqual(0, t[0], 'return code was not 0') self.assertEqual(0, t[0], 'return code was not 0')
def test_assert_python_expect_failure(self): def test_assert_python_failure(self):
# I didn't import the sys module so this child will fail. # I didn't import the sys module so this child will fail.
rc, out, err = script_helper._assert_python(False, '-c', 'sys.exit(0)') rc, out, err = script_helper.assert_python_failure('-c', 'sys.exit(0)')
self.assertNotEqual(0, rc, 'return code should not be 0') self.assertNotEqual(0, rc, 'return code should not be 0')
def test_assert_python_raises_expect_success(self): def test_assert_python_ok_raises(self):
# I didn't import the sys module so this child will fail. # I didn't import the sys module so this child will fail.
with self.assertRaises(AssertionError) as error_context: with self.assertRaises(AssertionError) as error_context:
script_helper._assert_python(True, '-c', 'sys.exit(0)') script_helper.assert_python_ok('-c', 'sys.exit(0)')
error_msg = str(error_context.exception) error_msg = str(error_context.exception)
self.assertIn('command line:', error_msg) self.assertIn('command line:', error_msg)
self.assertIn('sys.exit(0)', error_msg, msg='unexpected command line') self.assertIn('sys.exit(0)', error_msg, msg='unexpected command line')
def test_assert_python_raises_expect_failure(self): def test_assert_python_failure_raises(self):
with self.assertRaises(AssertionError) as error_context: with self.assertRaises(AssertionError) as error_context:
script_helper._assert_python(False, '-c', 'import sys; sys.exit(0)') script_helper.assert_python_failure('-c', 'import sys; sys.exit(0)')
error_msg = str(error_context.exception) error_msg = str(error_context.exception)
self.assertIn('Process return code is 0\n', error_msg) self.assertIn('Process return code is 0\n', error_msg)
self.assertIn('import sys; sys.exit(0)', error_msg, self.assertIn('import sys; sys.exit(0)', error_msg,

View File

@ -10,6 +10,11 @@ Release date: XXX
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #23309: Avoid a deadlock at shutdown if a daemon thread is aborted
while it is holding a lock to a buffered I/O object, and the main thread
tries to use the same I/O object (typically stdout or stderr). A fatal
error is emitted instead.
- Issue #22977: Fixed formatting Windows error messages on Wine. - Issue #22977: Fixed formatting Windows error messages on Wine.
Patch by Martin Panter. Patch by Martin Panter.

View File

@ -318,14 +318,35 @@ typedef struct {
static int static int
_enter_buffered_busy(buffered *self) _enter_buffered_busy(buffered *self)
{ {
int relax_locking;
PyLockStatus st;
if (self->owner == PyThread_get_thread_ident()) { if (self->owner == PyThread_get_thread_ident()) {
PyErr_Format(PyExc_RuntimeError, PyErr_Format(PyExc_RuntimeError,
"reentrant call inside %R", self); "reentrant call inside %R", self);
return 0; return 0;
} }
relax_locking = (_Py_Finalizing != NULL);
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
PyThread_acquire_lock(self->lock, 1); if (!relax_locking)
st = PyThread_acquire_lock(self->lock, 1);
else {
/* When finalizing, we don't want a deadlock to happen with daemon
* threads abruptly shut down while they owned the lock.
* Therefore, only wait for a grace period (1 s.).
* Note that non-daemon threads have already exited here, so this
* shouldn't affect carefully written threaded I/O code.
*/
st = PyThread_acquire_lock_timed(self->lock, 1e6, 0);
}
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS
if (relax_locking && st != PY_LOCK_ACQUIRED) {
PyObject *msgobj = PyUnicode_FromFormat(
"could not acquire lock for %A at interpreter "
"shutdown, possibly due to daemon threads",
(PyObject *) self);
char *msg = PyUnicode_AsUTF8(msgobj);
Py_FatalError(msg);
}
return 1; return 1;
} }