Skip to content

Commit b5711c9

Browse files
authored
bpo-37193: Remove thread objects which finished process its request (GH-23127)
This reverts commit aca67da.
1 parent 3631d6d commit b5711c9

File tree

3 files changed

+64
-12
lines changed

3 files changed

+64
-12
lines changed

Lib/socketserver.py

Lines changed: 39 additions & 12 deletions
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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ class MyHandler(socketserver.StreamRequestHandler):
277277
t.join()
278278
s.server_close()
279279

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

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

495518
if __name__ == "__main__":
496519
unittest.main()
Lines changed: 2 additions & 0 deletions
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)