Skip to content

Commit ffb4940

Browse files
authored
test_multiprocessing detects dangling per test case (#2841)
bpo-26762: test_multiprocessing now detects dangling processes and threads per test case classes: * setUpClass()/tearDownClass() of mixin classes now check if multiprocessing.process._dangling or threading._dangling was modified to detect "dangling" processses and threads. * ManagerMixin.tearDownClass() now also emits a warning if it still has more than one active child process after 5 seconds. * tearDownModule() now checks for dangling processes and threads before sleep 500 ms. And it now only sleeps if there is a least one dangling process or thread.
1 parent d7e64d9 commit ffb4940

File tree

1 file changed

+67
-14
lines changed

1 file changed

+67
-14
lines changed

Lib/test/_test_multiprocessing.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4303,7 +4303,32 @@ def test_empty(self):
43034303
# Mixins
43044304
#
43054305

4306-
class ProcessesMixin(object):
4306+
class BaseMixin(object):
4307+
@classmethod
4308+
def setUpClass(cls):
4309+
cls.dangling = (multiprocessing.process._dangling.copy(),
4310+
threading._dangling.copy())
4311+
4312+
@classmethod
4313+
def tearDownClass(cls):
4314+
# bpo-26762: Some multiprocessing objects like Pool create reference
4315+
# cycles. Trigger a garbage collection to break these cycles.
4316+
test.support.gc_collect()
4317+
4318+
processes = set(multiprocessing.process._dangling) - set(cls.dangling[0])
4319+
if processes:
4320+
print('Warning -- Dangling processes: %s' % processes,
4321+
file=sys.stderr)
4322+
processes = None
4323+
4324+
threads = set(threading._dangling) - set(cls.dangling[1])
4325+
if threads:
4326+
print('Warning -- Dangling threads: %s' % threads,
4327+
file=sys.stderr)
4328+
threads = None
4329+
4330+
4331+
class ProcessesMixin(BaseMixin):
43074332
TYPE = 'processes'
43084333
Process = multiprocessing.Process
43094334
connection = multiprocessing.connection
@@ -4326,7 +4351,7 @@ class ProcessesMixin(object):
43264351
RawArray = staticmethod(multiprocessing.RawArray)
43274352

43284353

4329-
class ManagerMixin(object):
4354+
class ManagerMixin(BaseMixin):
43304355
TYPE = 'manager'
43314356
Process = multiprocessing.Process
43324357
Queue = property(operator.attrgetter('manager.Queue'))
@@ -4350,30 +4375,43 @@ def Pool(cls, *args, **kwds):
43504375

43514376
@classmethod
43524377
def setUpClass(cls):
4378+
super().setUpClass()
43534379
cls.manager = multiprocessing.Manager()
43544380

43554381
@classmethod
43564382
def tearDownClass(cls):
43574383
# only the manager process should be returned by active_children()
43584384
# but this can take a bit on slow machines, so wait a few seconds
43594385
# if there are other children too (see #17395)
4386+
start_time = time.monotonic()
43604387
t = 0.01
4361-
while len(multiprocessing.active_children()) > 1 and t < 5:
4388+
while len(multiprocessing.active_children()) > 1:
43624389
time.sleep(t)
43634390
t *= 2
4391+
dt = time.monotonic() - start_time
4392+
if dt >= 5.0:
4393+
print("Warning -- multiprocessing.Manager still has %s active "
4394+
"children after %s seconds"
4395+
% (multiprocessing.active_children(), dt),
4396+
file=sys.stderr)
4397+
break
4398+
43644399
gc.collect() # do garbage collection
43654400
if cls.manager._number_of_objects() != 0:
43664401
# This is not really an error since some tests do not
43674402
# ensure that all processes which hold a reference to a
43684403
# managed object have been joined.
4369-
print('Shared objects which still exist at manager shutdown:')
4404+
print('Warning -- Shared objects which still exist at manager '
4405+
'shutdown:')
43704406
print(cls.manager._debug_info())
43714407
cls.manager.shutdown()
43724408
cls.manager.join()
43734409
cls.manager = None
43744410

4411+
super().tearDownClass()
4412+
43754413

4376-
class ThreadsMixin(object):
4414+
class ThreadsMixin(BaseMixin):
43774415
TYPE = 'threads'
43784416
Process = multiprocessing.dummy.Process
43794417
connection = multiprocessing.dummy.connection
@@ -4450,18 +4488,33 @@ def setUpModule():
44504488
multiprocessing.get_logger().setLevel(LOG_LEVEL)
44514489

44524490
def tearDownModule():
4491+
need_sleep = False
4492+
4493+
# bpo-26762: Some multiprocessing objects like Pool create reference
4494+
# cycles. Trigger a garbage collection to break these cycles.
4495+
test.support.gc_collect()
4496+
44534497
multiprocessing.set_start_method(old_start_method[0], force=True)
44544498
# pause a bit so we don't get warning about dangling threads/processes
4455-
time.sleep(0.5)
4499+
processes = set(multiprocessing.process._dangling) - set(dangling[0])
4500+
if processes:
4501+
need_sleep = True
4502+
print('Warning -- Dangling processes: %s' % processes,
4503+
file=sys.stderr)
4504+
processes = None
4505+
4506+
threads = set(threading._dangling) - set(dangling[1])
4507+
if threads:
4508+
need_sleep = True
4509+
print('Warning -- Dangling threads: %s' % threads,
4510+
file=sys.stderr)
4511+
threads = None
4512+
4513+
# Sleep 500 ms to give time to child processes to complete.
4514+
if need_sleep:
4515+
time.sleep(0.5)
44564516
multiprocessing.process._cleanup()
4457-
gc.collect()
4458-
tmp = set(multiprocessing.process._dangling) - set(dangling[0])
4459-
if tmp:
4460-
print('Dangling processes:', tmp, file=sys.stderr)
4461-
del tmp
4462-
tmp = set(threading._dangling) - set(dangling[1])
4463-
if tmp:
4464-
print('Dangling threads:', tmp, file=sys.stderr)
4517+
test.support.gc_collect()
44654518

44664519
remote_globs['setUpModule'] = setUpModule
44674520
remote_globs['tearDownModule'] = tearDownModule

0 commit comments

Comments
 (0)