Skip to content

Commit 43a4511

Browse files
authored
gh-159503: Avoid copying the unsent remainder of a bytes write in asyncio selector transports (#159504)
1 parent 7b727ab commit 43a4511

4 files changed

Lines changed: 25 additions & 8 deletions

File tree

‎Doc/library/asyncio-protocol.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ Write-only Transports
333333
This method does not block; it buffers the data and arranges for it
334334
to be sent out asynchronously.
335335

336+
The transport keeps a reference to *data* rather than copying. *data*
337+
must not be mutated while being sent.
338+
336339
.. method:: WriteTransport.writelines(list_of_data)
337340

338341
Write a list (or any iterable) of data bytes to the transport.

‎Lib/asyncio/selector_events.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,9 @@ def write(self, data):
10991099
# Not all was written; register write handler.
11001100
self._add_writer(self._sock_fd, self._write_ready)
11011101

1102-
# Add it to the buffer.
1102+
# Add it to the buffer as a view, so partial sends don't copy it.
1103+
if not isinstance(data, memoryview):
1104+
data = memoryview(data)
11031105
self._buffer.append(data)
11041106
self._buffer_size += len(data)
11051107
self._maybe_pause_protocol()

‎Lib/test/test_asyncio/test_selector_events.py‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -855,14 +855,23 @@ def test_write_partial_none(self):
855855
self.assertEqual(list_to_buffer([b'data']), transport._buffer)
856856

857857
def test_write_tryagain(self):
858-
self.sock.send.side_effect = BlockingIOError
859-
860858
data = b'data'
861-
transport = self.socket_transport()
862-
transport.write(data)
863-
864-
self.loop.assert_writer(7, transport._write_ready)
865-
self.assertEqual(list_to_buffer([b'data']), transport._buffer)
859+
for buf in data, bytearray(data), memoryview(data):
860+
with self.subTest(type=type(buf).__name__):
861+
self.sock.send.side_effect = BlockingIOError
862+
863+
transport = self.socket_transport()
864+
transport.write(buf)
865+
866+
self.loop.assert_writer(7, transport._write_ready)
867+
self.assertEqual(list_to_buffer([b'data']), transport._buffer)
868+
# gh-159503: the data is buffered as a view, not a copy
869+
self.assertIsInstance(transport._buffer[0], memoryview)
870+
self.assertIs(transport._buffer[0].obj, memoryview(buf).obj)
871+
if isinstance(buf, bytearray):
872+
# The buffered view pins the bytearray against resizing.
873+
with self.assertRaises(BufferError):
874+
buf.append(0)
866875

867876
def test_write_sendmsg_no_data(self):
868877
self.sock.sendmsg = mock.Mock()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix quadratic copying in :mod:`asyncio` selector socket transports on partial
2+
sends. A :class:`bytearray` passed to :meth:`~asyncio.WriteTransport.write`
3+
now stays locked against resizing until it has been sent.

0 commit comments

Comments
 (0)