Skip to content

Commit 7738d7c

Browse files
[3.15] gh-156512: Fix asyncio calling connection_lost() twice from resume_writing() (GH-156657) (#157031)
Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
1 parent 02f55f3 commit 7738d7c

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

Lib/asyncio/selector_events.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,9 @@ def _write_sendmsg(self):
11241124
self._loop._remove_writer(self._sock_fd)
11251125
if self._empty_waiter is not None:
11261126
self._empty_waiter.set_result(None)
1127-
if self._closing:
1127+
# gh-156512: don't let _call_connection_lost be called twice
1128+
if self._closing and not self._conn_lost:
1129+
self._conn_lost += 1
11281130
self._call_connection_lost(None)
11291131
elif self._eof:
11301132
self._sock.shutdown(socket.SHUT_WR)
@@ -1170,7 +1172,9 @@ def _write_send(self):
11701172
self._loop._remove_writer(self._sock_fd)
11711173
if self._empty_waiter is not None:
11721174
self._empty_waiter.set_result(None)
1173-
if self._closing:
1175+
# gh-156512: don't let _call_connection_lost be called twice
1176+
if self._closing and not self._conn_lost:
1177+
self._conn_lost += 1
11741178
self._call_connection_lost(None)
11751179
elif self._eof:
11761180
self._sock.shutdown(socket.SHUT_WR)

Lib/test/test_asyncio/test_selector_events.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,49 @@ def _resume_writing():
11221122
self.assertEqual(transport.get_write_buffer_size(), 0)
11231123
self.assertTrue(self.protocol.connection_lost.called)
11241124

1125+
def test_write_ready_resume_writing_closes(self):
1126+
# gh-156512: closing from resume_writing() must not lose the connection twice
1127+
self.sock.send.return_value = 2
1128+
1129+
def _resume_writing():
1130+
transport.close()
1131+
1132+
self.protocol.resume_writing.side_effect = _resume_writing
1133+
self.loop.call_exception_handler = mock.Mock()
1134+
1135+
transport = self.socket_transport()
1136+
transport.set_write_buffer_limits(high=1, low=0)
1137+
transport.write(b'data')
1138+
1139+
self.loop.writers[7]._run()
1140+
test_utils.run_briefly(self.loop)
1141+
1142+
self.assertEqual(self.protocol.connection_lost.call_count, 1)
1143+
self.loop.call_exception_handler.assert_not_called()
1144+
1145+
@unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg')
1146+
def test_write_sendmsg_resume_writing_closes(self):
1147+
# gh-156512: same as above, for the sendmsg write path
1148+
self.sock.send.return_value = 2
1149+
self.sock.sendmsg.return_value = 2
1150+
1151+
def _resume_writing():
1152+
transport.close()
1153+
1154+
self.protocol.resume_writing.side_effect = _resume_writing
1155+
self.loop.call_exception_handler = mock.Mock()
1156+
1157+
transport = self.socket_transport(sendmsg=True)
1158+
transport.set_write_buffer_limits(high=1, low=0)
1159+
transport.write(b'data')
1160+
1161+
self.loop.writers[7]._run()
1162+
test_utils.run_briefly(self.loop)
1163+
1164+
self.assertEqual(self.protocol.connection_lost.call_count, 1)
1165+
self.loop.call_exception_handler.assert_not_called()
1166+
1167+
11251168
class SelectorSocketTransportBufferedProtocolTests(test_utils.TestCase):
11261169

11271170
def setUp(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`asyncio` losing a connection twice when ``resume_writing()``
2+
closes the transport.

0 commit comments

Comments
 (0)