From a2224379db7b4ef0363942b92a07ad0e1411601c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Jul 2026 14:24:12 +0300 Subject: [PATCH 1/2] gh-84532: Reopen the socket in SocketHandler.send() when sending fails It dropped the record whose send failed and reconnected only on the next record. Now it reopens the socket and sends the record again. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/logging/handlers.py | 19 ++++++---- Lib/test/test_logging.py | 38 +++++++++++++++++++ ...6-07-23-08-46-07.gh-issue-84532.NSN7fJ.rst | 2 + 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-08-46-07.gh-issue-84532.NSN7fJ.rst diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index a5394d2dbea6494..a0397266b654f36 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -657,15 +657,20 @@ def send(self, s): This function allows for partial sends which can happen when the network is busy. """ - if self.sock is None: - self.createSocket() - #self.sock can be None either because we haven't reached the retry - #time yet, or because we have reached the retry time and retried, - #but are still unable to connect. - if self.sock: + # Reopen the socket once if sending fails, e.g. the connection was + # closed after a timeout, so the record is not lost (gh-84532). + for attempt in range(2): + if self.sock is None: + self.createSocket() + # self.sock can be None either because we haven't reached the + # retry time yet, or because we have reached the retry time + # and retried, but are still unable to connect. + if self.sock is None: + return try: self.sock.sendall(s) - except OSError: #pragma: no cover + return + except OSError: self.sock.close() self.sock = None # so we can call createSocket next time diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 06b3aa66fc47a31..4d5fd49b3fe50b6 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -38,6 +38,7 @@ import queue import random import re +import select import shutil import socket import struct @@ -1940,6 +1941,43 @@ def test_noserver(self): time.sleep(self.sock_hdlr.retryTime - now + 0.001) self.root_logger.error('Nor this') + def test_reconnect(self): + # If the server closes the connection, the record whose send fails is + # not dropped, but resent on a reopened socket (gh-84532). + server = socket.create_server(('localhost', 0)) + self.addCleanup(server.close) + server.settimeout(support.LONG_TIMEOUT) + port = server.getsockname()[1] + hdlr = logging.handlers.SocketHandler('localhost', port) + self.addCleanup(hdlr.close) + + def recv_message(): + conn, _ = server.accept() + chunk = conn.recv(4) + slen = struct.unpack(">L", chunk)[0] + chunk = b'' + while len(chunk) < slen: + chunk += conn.recv(slen - len(chunk)) + return conn, logging.makeLogRecord(pickle.loads(chunk)).msg + + # Connect and receive the first record. + hdlr.handle(logging.makeLogRecord({'msg': 'spam'})) + conn, msg = recv_message() + self.assertEqual(msg, 'spam') + + # Close the connection on the server side. After a peer close the + # first send still succeeds (the data is lost when the reset arrives), + # so a throwaway send is used up here to make the next send fail. + conn.close() + # Wait until the close is received, so the sends below are ordered. + select.select([hdlr.sock], [], [], support.LONG_TIMEOUT) + hdlr.sock.sendall(b'\x00\x00\x00\x00') + + # The next send fails and the record is resent on a new connection. + hdlr.handle(logging.makeLogRecord({'msg': 'eggs'})) + conn, msg = recv_message() + self.assertEqual(msg, 'eggs') + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") class UnixSocketHandlerTest(SocketHandlerTest): diff --git a/Misc/NEWS.d/next/Library/2026-07-23-08-46-07.gh-issue-84532.NSN7fJ.rst b/Misc/NEWS.d/next/Library/2026-07-23-08-46-07.gh-issue-84532.NSN7fJ.rst new file mode 100644 index 000000000000000..77a838f0ec1a6a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-08-46-07.gh-issue-84532.NSN7fJ.rst @@ -0,0 +1,2 @@ +:class:`logging.handlers.SocketHandler` now loses fewer log records when the +connection to the server is closed, for example after an idle timeout. From fdcdb4f8ab60200a9062045fc5d8ed4527fac625 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Jul 2026 17:53:14 +0300 Subject: [PATCH 2/2] gh-84532: Close the accepted connections in test_reconnect The test left the server-side connections unclosed, which regrtest reported as an altered environment (ResourceWarning: unclosed socket). Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_logging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 4d5fd49b3fe50b6..3b2dd21f4686bbf 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -1953,6 +1953,7 @@ def test_reconnect(self): def recv_message(): conn, _ = server.accept() + self.addCleanup(conn.close) chunk = conn.recv(4) slen = struct.unpack(">L", chunk)[0] chunk = b''