diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index a5394d2dbea649..a0397266b654f3 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 06b3aa66fc47a3..3b2dd21f4686bb 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,44 @@ 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() + self.addCleanup(conn.close) + 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 00000000000000..77a838f0ec1a6a --- /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.