Skip to content

Commit 0064d56

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

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
@@ -276,6 +276,13 @@ class MyHandler(socketserver.StreamRequestHandler):
276276
t.join()
277277
s.server_close()
278278

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

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

494517
if __name__ == "__main__":
495518
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)