From 20bd1cc507ed08354597ec7261bfc67e1b8c6164 Mon Sep 17 00:00:00 2001 From: Bruce Merry Date: Thu, 17 Jul 2025 16:16:45 +0200 Subject: [PATCH 1/2] Fix _SelectorSocketTransport.writelines to be robust to connection loss Closes GH-136234 --- Lib/asyncio/selector_events.py | 14 +++++++++++--- Lib/test/test_asyncio/test_selector_events.py | 16 ++++++++++++++++ ...025-07-17-16-12-23.gh-issue-136234.VmTxtj.rst | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-17-16-12-23.gh-issue-136234.VmTxtj.rst diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 6ad84044adf146..109f3d9300b368 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1048,6 +1048,11 @@ def _read_ready__on_eof(self): else: self.close() + def _write_after_conn_lost(self): + if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: + logger.warning('socket.send() raised exception.') + self._conn_lost += 1 + def write(self, data): if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError(f'data argument must be a bytes-like object, ' @@ -1060,9 +1065,7 @@ def write(self, data): return if self._conn_lost: - if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: - logger.warning('socket.send() raised exception.') - self._conn_lost += 1 + self._write_after_conn_lost() return if not self._buffer: @@ -1174,6 +1177,11 @@ def writelines(self, list_of_data): raise RuntimeError('unable to writelines; sendfile is in progress') if not list_of_data: return + + if self._conn_lost: + self._write_after_conn_lost() + return + self._buffer.extend([memoryview(data) for data in list_of_data]) self._write_ready() # If the entire buffer couldn't be written, register a write handler diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index 7b6d1bce5e460f..5a7df774e60858 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -854,6 +854,22 @@ def test_writelines_pauses_protocol(self): self.assertTrue(self.sock.send.called) self.assertTrue(self.loop.writers) + def test_writelines_after_connection_lost(self): + # GH-136234 + transport = self.socket_transport() + self.sock.send = mock.Mock() + self.sock.send.side_effect = ConnectionResetError + transport.write(b'data1') # Will fail immediately, causing connection lost + + transport.writelines([b'data2']) + self.assertFalse(transport._buffer) + self.assertFalse(self.loop.writers) + + test_utils.run_briefly(self.loop) # Allow _call_connection_lost to run + transport.writelines([b'data2']) + self.assertFalse(transport._buffer) + self.assertFalse(self.loop.writers) + @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_full(self): data = memoryview(b'data') diff --git a/Misc/NEWS.d/next/Library/2025-07-17-16-12-23.gh-issue-136234.VmTxtj.rst b/Misc/NEWS.d/next/Library/2025-07-17-16-12-23.gh-issue-136234.VmTxtj.rst new file mode 100644 index 00000000000000..044a601c9170a1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-17-16-12-23.gh-issue-136234.VmTxtj.rst @@ -0,0 +1,2 @@ +Fix :meth:`asyncio.WriteTransport.writelines` to be robust to connection +failure, by using the same behavior as :meth:`~asyncio.WriteTransport.write`. From 74ea9900d9a46216f3a38adfd80eed491c34bc17 Mon Sep 17 00:00:00 2001 From: Bruce Merry Date: Mon, 21 Jul 2025 10:02:08 +0200 Subject: [PATCH 2/2] Inline _write_after_conn_lost --- Lib/asyncio/selector_events.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 109f3d9300b368..dd82c1d2340261 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1048,11 +1048,6 @@ def _read_ready__on_eof(self): else: self.close() - def _write_after_conn_lost(self): - if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: - logger.warning('socket.send() raised exception.') - self._conn_lost += 1 - def write(self, data): if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError(f'data argument must be a bytes-like object, ' @@ -1065,7 +1060,9 @@ def write(self, data): return if self._conn_lost: - self._write_after_conn_lost() + if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: + logger.warning('socket.send() raised exception.') + self._conn_lost += 1 return if not self._buffer: @@ -1179,7 +1176,9 @@ def writelines(self, list_of_data): return if self._conn_lost: - self._write_after_conn_lost() + if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES: + logger.warning('socket.send() raised exception.') + self._conn_lost += 1 return self._buffer.extend([memoryview(data) for data in list_of_data])