mirror of https://github.com/python/cpython
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:
commit
cb46f0ecb0
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue