Refs #27734 -- Prevented creation of more parallel workers than TestCases.

The parallel test runner uses multiple workers to distribute the
workload. These workers are assigned a worker ID using a globally
incremented variable, which determines what test database to connect
to. When the worker ID surpasses the test database IDs Django will
crash.

This reduce likelihood of crashing parallel tests because
ParallelTestSuite will no longer create more workers than TestCases.

It won't eliminate the problem completely though because there are
other circumstances in which new workers can be created which can then
be assigned an "illegal" worker ID.
This commit is contained in:
Ceesjan Luiten 2021-05-21 16:49:01 +02:00 committed by Mariusz Felisiak
parent ed3af3ff4b
commit cb6c19749d
2 changed files with 17 additions and 16 deletions

View File

@ -404,8 +404,8 @@ class ParallelTestSuite(unittest.TestSuite):
run_subsuite = _run_subsuite run_subsuite = _run_subsuite
runner_class = RemoteTestRunner runner_class = RemoteTestRunner
def __init__(self, suite, processes, failfast=False, buffer=False): def __init__(self, subsuites, processes, failfast=False, buffer=False):
self.subsuites = partition_suite_by_case(suite) self.subsuites = subsuites
self.processes = processes self.processes = processes
self.failfast = failfast self.failfast = failfast
self.buffer = buffer self.buffer = buffer
@ -685,22 +685,17 @@ class DiscoverRunner:
suite = self.test_suite(all_tests) suite = self.test_suite(all_tests)
if self.parallel > 1: if self.parallel > 1:
parallel_suite = self.parallel_test_suite( subsuites = partition_suite_by_case(suite)
suite, # Since tests are distributed across processes on a per-TestCase
self.parallel, # basis, there's no need for more processes than TestCases.
processes = min(self.parallel, len(subsuites))
if processes > 1:
suite = self.parallel_test_suite(
subsuites,
processes,
self.failfast, self.failfast,
self.buffer, self.buffer,
) )
# Since tests are distributed across processes on a per-TestCase
# basis, there's no need for more processes than TestCases.
parallel_units = len(parallel_suite.subsuites)
self.parallel = min(self.parallel, parallel_units)
# If there's only one TestCase, parallelization isn't needed.
if self.parallel > 1:
suite = parallel_suite
return suite return suite
def setup_databases(self, **kwargs): def setup_databases(self, **kwargs):

View File

@ -348,6 +348,12 @@ class DiscoverRunnerTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
DiscoverRunner(pdb=True, parallel=2) DiscoverRunner(pdb=True, parallel=2)
def test_number_of_parallel_workers(self):
"""Number of processes doesn't exceed the number of TestCases."""
runner = DiscoverRunner(parallel=5, verbosity=0)
suite = runner.build_suite(['test_runner_apps.tagged'])
self.assertEqual(suite.processes, len(suite.subsuites))
def test_buffer_mode_test_pass(self): def test_buffer_mode_test_pass(self):
runner = DiscoverRunner(buffer=True, verbose=0) runner = DiscoverRunner(buffer=True, verbose=0)
with captured_stdout() as stdout, captured_stderr() as stderr: with captured_stdout() as stdout, captured_stderr() as stderr: