Skip to content

Commit edcafcf

Browse files
committed
Merge branch 'pythongh-104090-leaked-semaphore-objects-in-test_concurrent_futures-debug-session' into pythongh-104090-fix-leaked-semaphors-on-test_concurrent_futures
2 parents 09e72be + 9923f46 commit edcafcf

File tree

10 files changed

+138
-5
lines changed

10 files changed

+138
-5
lines changed

Lib/concurrent/futures/process.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ def __init__(self, max_workers=None, mp_context=None,
723723
# tracebacks in the queue's own worker thread. But we detect killed
724724
# processes anyway, so silence the tracebacks.
725725
self._call_queue._ignore_epipe = True
726+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ProcessPoolExecutor.__init__: mp_context.SimpleQueue()")
726727
self._result_queue = mp_context.SimpleQueue()
727728
self._work_ids = queue.Queue()
728729

Lib/multiprocessing/queues.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ def __init__(self, maxsize=0, *, ctx):
4040
from .synchronize import SEM_VALUE_MAX as maxsize
4141
self._maxsize = maxsize
4242
self._reader, self._writer = connection.Pipe(duplex=False)
43+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] Queue __init__: Set self._rlock = ctx.Lock()")
4344
self._rlock = ctx.Lock()
4445
self._opid = os.getpid()
4546
if sys.platform == 'win32':
4647
self._wlock = None
4748
else:
49+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] Queue __init__: Set self._wlock = ctx.Lock()")
4850
self._wlock = ctx.Lock()
51+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] Queue __init__: Set ctx.BoundedSemaphore(maxsize)")
4952
self._sem = ctx.BoundedSemaphore(maxsize)
5053
# For use by concurrent.futures
5154
self._ignore_epipe = False
@@ -345,6 +348,7 @@ def __init__(self, *, ctx):
345348
if sys.platform == 'win32':
346349
self._wlock = None
347350
else:
351+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] SimpleQueue.__init__: self._wlock = ctx.Lock()")
348352
self._wlock = ctx.Lock()
349353

350354
def close(self):

Lib/multiprocessing/resource_tracker.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@
3333
'noop': lambda: None,
3434
}
3535

36+
def semwrapper():
37+
def inner(name):
38+
with open("/dev/stdin", "w") as sys.stdin:
39+
with open("/dev/stdout", "w") as sys.stdout:
40+
# import traceback
41+
# traceback.print_stack()
42+
_multiprocessing.sem_unlink(name)
43+
return inner
44+
45+
3646
if os.name == 'posix':
3747
import _multiprocessing
3848
import _posixshmem
@@ -43,8 +53,11 @@
4353
# absence of POSIX named semaphores. In that case, no named semaphores were
4454
# ever opened, so no cleanup would be necessary.
4555
if hasattr(_multiprocessing, 'sem_unlink'):
56+
# import traceback
57+
# traceback.print_stack()
58+
# print("add _CLEANUP_FUNCS for semaphore")
4659
_CLEANUP_FUNCS.update({
47-
'semaphore': _multiprocessing.sem_unlink,
60+
'semaphore': semwrapper(), #_multiprocessing.sem_unlink,
4861
})
4962
_CLEANUP_FUNCS.update({
5063
'shared_memory': _posixshmem.shm_unlink,
@@ -65,6 +78,8 @@ def _stop(self):
6578
return
6679

6780
# closing the "alive" file descriptor stops main()
81+
import os, threading
82+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ResourceTracker._stop: os.close(self._fd)")
6883
os.close(self._fd)
6984
self._fd = None
7085

@@ -151,11 +166,13 @@ def _check_alive(self):
151166
return True
152167

153168
def register(self, name, rtype):
169+
# print(f"send UNREGISTER {name} {rtype}")
154170
'''Register name of resource with resource tracker.'''
155171
self._send('REGISTER', name, rtype)
156172

157173
def unregister(self, name, rtype):
158174
'''Unregister name of resource with resource tracker.'''
175+
# print(f"send UNREGISTER {name} {rtype}")
159176
self._send('UNREGISTER', name, rtype)
160177

161178
def _send(self, cmd, name, rtype):
@@ -177,6 +194,9 @@ def _send(self, cmd, name, rtype):
177194
getfd = _resource_tracker.getfd
178195

179196
def main(fd):
197+
# import pdb; pdb.set_trace()
198+
# import traceback
199+
# traceback.print_stack()
180200
'''Run resource tracker.'''
181201
# protect the process from ^C and "killall python" etc
182202
signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -197,15 +217,21 @@ def main(fd):
197217
for line in f:
198218
try:
199219
cmd, name, rtype = line.strip().decode('ascii').split(':')
220+
with open("/dev/stdin", "w") as sys.stdin, open("/dev/stdout", "w") as sys.stdout:
221+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] {cmd=}, {name=}, {rtype=}")
200222
cleanup_func = _CLEANUP_FUNCS.get(rtype, None)
201223
if cleanup_func is None:
202224
raise ValueError(
203225
f'Cannot register {name} for automatic cleanup: '
204226
f'unknown resource type {rtype}')
205227

206228
if cmd == 'REGISTER':
229+
# warnings.warn(f"REGISTER:{rtype}:{name}:{fd}")
207230
cache[rtype].add(name)
208231
elif cmd == 'UNREGISTER':
232+
# import traceback
233+
# traceback.print_stack()
234+
# warnings.warn(f"UNREGISTER:{rtype}:{name}:{fd}")
209235
cache[rtype].remove(name)
210236
elif cmd == 'PROBE':
211237
pass
@@ -225,13 +251,15 @@ def main(fd):
225251
'leaked %s objects to clean up at shutdown' %
226252
(len(rtype_cache), rtype))
227253
except Exception:
228-
pass
254+
raise
229255
for name in rtype_cache:
230256
# For some reason the process which created and registered this
231257
# resource has failed to unregister it. Presumably it has
232258
# died. We therefore unlink it.
233259
try:
234260
try:
261+
with open("/dev/stdin", "w") as sys.stdin, open("/dev/stdout", "w") as sys.stdout:
262+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] call _CLEANUP_FUNCS {rtype} function for {name} file")
235263
_CLEANUP_FUNCS[rtype](name)
236264
except Exception as e:
237265
warnings.warn('resource_tracker: %r: %s' % (name, e))

Lib/multiprocessing/synchronize.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ class SemLock(object):
4848
_rand = tempfile._RandomNameSequence()
4949

5050
def __init__(self, kind, value, maxvalue, *, ctx):
51+
# import pdb; pdb.set_trace()
52+
# print(f"new semlock {self.__class__.__name__=} {kind=} {value=} {maxvalue=} {ctx=}")
5153
if ctx is None:
5254
ctx = context._default_context.get_context()
5355
name = ctx.get_start_method()
56+
# print(f"new semlock {name=}")
5457
unlink_now = sys.platform == 'win32' or name == 'fork'
5558
for i in range(100):
5659
try:
@@ -70,9 +73,12 @@ def __init__(self, kind, value, maxvalue, *, ctx):
7073
if sys.platform != 'win32':
7174
def _after_fork(obj):
7275
obj._semlock._after_fork()
76+
# print(f"Add semlock register_after_fork {self._semlock.name=}, {self.__class__.__name__=}")
7377
util.register_after_fork(self, _after_fork)
7478

7579
if self._semlock.name is not None:
80+
import os, threading
81+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] Add semlock to register and finalize {self._semlock.name=}, {self.__class__.__name__=}")
7682
# We only get here if we are on Unix with forking
7783
# disabled. When the object is garbage collected or the
7884
# process shuts down we unlink the semaphore name
@@ -84,6 +90,29 @@ def _after_fork(obj):
8490
@staticmethod
8591
def _cleanup(name):
8692
from .resource_tracker import unregister
93+
# import pdb; pdb.set_trace()
94+
import traceback, os
95+
tt="".join(traceback.format_stack())
96+
# import pdb; pdb.set_trace()
97+
if "_callTearDown" in tt:
98+
# traceback.print_stack()
99+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] case.py(638)run() -> self._callTearDown() -> run sem_unlink(name) on {name}")
100+
elif "_handleModuleTearDown" in tt:
101+
# traceback.print_stack()
102+
# import pdb; pdb.set_trace()
103+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] _run_finalizers() -> finalizer() -> run sem_unlink(name) on {name}")
104+
elif "suite.py\", line 122" in tt or "suite.py\", line 123" in tt or "suite.py\", line 124" in tt or "suite.py\", line 125" in tt or "suite.py\", line 126" in tt:
105+
# import pdb; pdb.set_trace()
106+
# import signal
107+
# os.kill(os.getpid(), signal.SIGUSR1)
108+
# traceback.print_stack()
109+
# breakpoint()
110+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] suite.py(122)test(result) -> suite.py(84)return self.run(*args, **kwds) -> run sem_unlink(name) on {name}")
111+
# pass
112+
else:
113+
# import pdb; pdb.set_trace()
114+
# traceback.print_stack()
115+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ERROR: UNKNOEN TRACE: run sem_unlink(name) on {name}")
87116
sem_unlink(name)
88117
unregister(name, "semaphore")
89118

@@ -92,9 +121,11 @@ def _make_methods(self):
92121
self.release = self._semlock.release
93122

94123
def __enter__(self):
124+
print(f"enter semlock {self.__class__.__name__}")
95125
return self._semlock.__enter__()
96126

97127
def __exit__(self, *args):
128+
print(f"exit semlock {self.__class__.__name__}")
98129
return self._semlock.__exit__(*args)
99130

100131
def __getstate__(self):

Lib/multiprocessing/util.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def _run_after_forkers():
169169
info('after forker raised exception %s', e)
170170

171171
def register_after_fork(obj, func):
172+
# print(f"register_after_fork: {obj=}, {func=}, {id(obj)=}")
172173
_afterfork_registry[(next(_afterfork_counter), id(obj), func)] = obj
173174

174175
#
@@ -221,6 +222,16 @@ def __call__(self, wr=None,
221222
else:
222223
sub_debug('finalizer calling %s with args %s and kwargs %s',
223224
self._callback, self._args, self._kwargs)
225+
if str(self._callback.__name__) == "_cleanup":
226+
import os, threading
227+
print(
228+
f"[pid={os.getpid()}, tid={threading.get_native_id()}] Finalize called on {self._callback=} {self._args=} {self._kwargs=}")
229+
# import pdb; pdb.set_trace()
230+
import os, threading, _multiprocessing, gc, sys, multiprocessing
231+
# for sl in [x for x in gc.get_objects() if isinstance(x, multiprocessing.queues.Queue)]:
232+
# print(sl, sys.getrefcount(sl))
233+
234+
# import pdb; pdb.set_trace()
224235
res = self._callback(*self._args, **self._kwargs)
225236
self._weakref = self._callback = self._args = \
226237
self._kwargs = self._key = None
@@ -295,6 +306,7 @@ def _run_finalizers(minpriority=None):
295306
finalizer = _finalizer_registry.get(key)
296307
# key may have been removed from the registry
297308
if finalizer is not None:
309+
# import pdb; pdb.set_trace()
298310
sub_debug('calling %s', finalizer)
299311
try:
300312
finalizer()
@@ -465,7 +477,6 @@ def close_fds(*fds):
465477
for fd in fds:
466478
os.close(fd)
467479

468-
469480
def _cleanup_tests():
470481
"""Cleanup multiprocessing resources when multiprocessing tests
471482
completed."""
@@ -479,13 +490,17 @@ def _cleanup_tests():
479490
from multiprocessing import forkserver
480491
forkserver._forkserver._stop()
481492

493+
# import pdb; pdb.set_trace()
494+
482495
# Stop the ResourceTracker process if it's running
496+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] Stop the ResourceTracker process if it's running")
483497
from multiprocessing import resource_tracker
484498
resource_tracker._resource_tracker._stop()
485499

486500
# bpo-37421: Explicitly call _run_finalizers() to remove immediately
487501
# temporary directories created by multiprocessing.util.get_temp_dir().
488502
_run_finalizers()
503+
489504
support.gc_collect()
490505

491506
support.reap_children()

Lib/test/test_concurrent_futures.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
2+
13
from test import support
24
from test.support import import_helper
35
from test.support import threading_helper
@@ -120,6 +122,8 @@ class ExecutorMixin:
120122
executor_kwargs = {}
121123

122124
def setUp(self):
125+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ###########")
126+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ExecutorMixin.__setUp start")
123127
super().setUp()
124128

125129
self.t1 = time.monotonic()
@@ -132,8 +136,12 @@ def setUp(self):
132136
self.executor = self.executor_type(
133137
max_workers=self.worker_count,
134138
**self.executor_kwargs)
139+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ExecutorMixin.__setUp end")
140+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ###########")
135141

136142
def tearDown(self):
143+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ###########")
144+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ExecutorMixin.tearDown start")
137145
self.executor.shutdown(wait=True)
138146
self.executor = None
139147

@@ -143,6 +151,8 @@ def tearDown(self):
143151
self.assertLess(dt, 300, "synchronization issue: test lasted too long")
144152

145153
super().tearDown()
154+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ExecutorMixin.tearDown end")
155+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ###########")
146156

147157
def get_context(self):
148158
return mp.get_context(self.ctx)
@@ -237,7 +247,10 @@ def setUp(self):
237247
if hasattr(self, "ctx"):
238248
# Pass a queue to redirect the child's logging output
239249
self.mp_context = self.get_context()
250+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ################")
251+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] FailingInitializerMixin.setUp: Calling self.mp_context.Queue()")
240252
self.log_queue = self.mp_context.Queue()
253+
print(f"[pid={os.getpid()}, tid={threading.get_native_id()}] ################")
241254
self.executor_kwargs = dict(initializer=init_fail,
242255
initargs=(self.log_queue,))
243256
else:
@@ -248,6 +261,14 @@ def setUp(self):
248261
self.executor_kwargs = dict(initializer=init_fail)
249262
super().setUp()
250263

264+
# def tearDown(self):
265+
# print("###########")
266+
# print("FailingInitializerMixin.tearDown start")
267+
# del self.log_queue
268+
# super().tearDown()
269+
# print("FailingInitializerMixin.tearDown end")
270+
# print("###########")
271+
251272
def test_initializer(self):
252273
with self._assert_logged('ValueError: error in initializer'):
253274
try:
@@ -275,8 +296,12 @@ def _assert_logged(self, msg):
275296
yield
276297
output = []
277298
try:
299+
# print("############## _assert_logged: while True start")
278300
while True:
279-
output.append(self.log_queue.get_nowait().getMessage())
301+
new_msg = self.log_queue.get_nowait().getMessage()
302+
# print(f"_assert_logged.get_Message: {new_msg}")
303+
output.append(new_msg)
304+
# print("############## _assert_logged: while True end")
280305
except queue.Empty:
281306
pass
282307
else:

Lib/unittest/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Main entry point"""
2-
2+
breakpoint()
33
import sys
44
if sys.argv[0].endswith("__main__.py"):
55
import os.path

0 commit comments

Comments
 (0)