Skip to content

Commit 0e76157

Browse files
[3.9] bpo-37193: Remove thread objects which finished process its request (GH-23127) (GH-24750)
This reverts commit aca67da. (cherry picked from commit b5711c9) Co-authored-by: Jason R. Coombs <[email protected]> Automerge-Triggered-By: GH:jaraco
1 parent ff6a021 commit 0e76157

File tree

3 files changed

+64
-12
lines changed

3 files changed

+64
-12
lines changed

Lib/socketserver.py

+39-12
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,39 @@ def server_close(self):
628628
self.collect_children(blocking=self.block_on_close)
629629

630630

631+
class _Threads(list):
632+
"""
633+
Joinable list of all non-daemon threads.
634+
"""
635+
def append(self, thread):
636+
self.reap()
637+
if thread.daemon:
638+
return
639+
super().append(thread)
640+
641+
def pop_all(self):
642+
self[:], result = [], self[:]
643+
return result
644+
645+
def join(self):
646+
for thread in self.pop_all():
647+
thread.join()
648+
649+
def reap(self):
650+
self[:] = (thread for thread in self if thread.is_alive())
651+
652+
653+
class _NoThreads:
654+
"""
655+
Degenerate version of _Threads.
656+
"""
657+
def append(self, thread):
658+
pass
659+
660+
def join(self):
661+
pass
662+
663+
631664
class ThreadingMixIn:
632665
"""Mix-in class to handle each request in a new thread."""
633666

@@ -636,9 +669,9 @@ class ThreadingMixIn:
636669
daemon_threads = False
637670
# If true, server_close() waits until all non-daemonic threads terminate.
638671
block_on_close = True
639-
# For non-daemonic threads, list of threading.Threading objects
672+
# Threads object
640673
# used by server_close() to wait for all threads completion.
641-
_threads = None
674+
_threads = _NoThreads()
642675

643676
def process_request_thread(self, request, client_address):
644677
"""Same as in BaseServer but as a thread.
@@ -655,23 +688,17 @@ def process_request_thread(self, request, client_address):
655688

656689
def process_request(self, request, client_address):
657690
"""Start a new thread to process the request."""
691+
if self.block_on_close:
692+
vars(self).setdefault('_threads', _Threads())
658693
t = threading.Thread(target = self.process_request_thread,
659694
args = (request, client_address))
660695
t.daemon = self.daemon_threads
661-
if not t.daemon and self.block_on_close:
662-
if self._threads is None:
663-
self._threads = []
664-
self._threads.append(t)
696+
self._threads.append(t)
665697
t.start()
666698

667699
def server_close(self):
668700
super().server_close()
669-
if self.block_on_close:
670-
threads = self._threads
671-
self._threads = None
672-
if threads:
673-
for thread in threads:
674-
thread.join()
701+
self._threads.join()
675702

676703

677704
if hasattr(os, "fork"):

Lib/test/test_socketserver.py

+23
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ class MyHandler(socketserver.StreamRequestHandler):
275275
t.join()
276276
s.server_close()
277277

278+
def test_close_immediately(self):
279+
class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
280+
pass
281+
282+
server = MyServer((HOST, 0), lambda: None)
283+
server.server_close()
284+
278285
def test_tcpserver_bind_leak(self):
279286
# Issue #22435: the server socket wouldn't be closed if bind()/listen()
280287
# failed.
@@ -489,6 +496,22 @@ def shutdown_request(self, request):
489496
self.assertEqual(server.shutdown_called, 1)
490497
server.server_close()
491498

499+
def test_threads_reaped(self):
500+
"""
501+
In #37193, users reported a memory leak
502+
due to the saving of every request thread. Ensure that
503+
not all threads are kept forever.
504+
"""
505+
class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
506+
pass
507+
508+
server = MyServer((HOST, 0), socketserver.StreamRequestHandler)
509+
for n in range(10):
510+
with socket.create_connection(server.server_address):
511+
server.handle_request()
512+
self.assertLess(len(server._threads), 10)
513+
server.server_close()
514+
492515

493516
if __name__ == "__main__":
494517
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed memory leak in ``socketserver.ThreadingMixIn`` introduced in Python
2+
3.7.

0 commit comments

Comments
 (0)