[3.13] gh-135444: fix asyncio.DatagramTransport.sendto to account for datagram header size when data cannot be sent (GH-135445) (#137246)

gh-135444: fix `asyncio.DatagramTransport.sendto` to account for datagram header size when data cannot be sent (GH-135445)
(cherry picked from commit e3ea861351)

Co-authored-by: Justin Bronder <jsbronder@cold-front.org>
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
This commit is contained in:
Miss Islington (bot) 2025-08-03 06:48:53 +02:00 committed by GitHub
parent 3b28cb0f3f
commit 1b0dfbf209
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 53 additions and 5 deletions

View file

@ -460,6 +460,8 @@ class _ProactorWritePipeTransport(_ProactorBaseWritePipeTransport):
class _ProactorDatagramTransport(_ProactorBasePipeTransport,
transports.DatagramTransport):
max_size = 256 * 1024
_header_size = 8
def __init__(self, loop, sock, protocol, address=None,
waiter=None, extra=None):
self._address = address
@ -499,7 +501,7 @@ class _ProactorDatagramTransport(_ProactorBasePipeTransport,
# Ensure that what we buffer is immutable.
self._buffer.append((bytes(data), addr))
self._buffer_size += len(data) + 8 # include header bytes
self._buffer_size += len(data) + self._header_size
if self._write_fut is None:
# No current write operations are active, kick one off
@ -526,7 +528,7 @@ class _ProactorDatagramTransport(_ProactorBasePipeTransport,
return
data, addr = self._buffer.popleft()
self._buffer_size -= len(data)
self._buffer_size -= len(data) + self._header_size
if self._address is not None:
self._write_fut = self._loop._proactor.send(self._sock,
data)

View file

@ -1208,6 +1208,7 @@ class _SelectorSocketTransport(_SelectorTransport):
class _SelectorDatagramTransport(_SelectorTransport, transports.DatagramTransport):
_buffer_factory = collections.deque
_header_size = 8
def __init__(self, loop, sock, protocol, address=None,
waiter=None, extra=None):
@ -1281,13 +1282,13 @@ class _SelectorDatagramTransport(_SelectorTransport, transports.DatagramTranspor
# Ensure that what we buffer is immutable.
self._buffer.append((bytes(data), addr))
self._buffer_size += len(data) + 8 # include header bytes
self._buffer_size += len(data) + self._header_size
self._maybe_pause_protocol()
def _sendto_ready(self):
while self._buffer:
data, addr = self._buffer.popleft()
self._buffer_size -= len(data)
self._buffer_size -= len(data) + self._header_size
try:
if self._extra['peername']:
self._sock.send(data)
@ -1295,7 +1296,7 @@ class _SelectorDatagramTransport(_SelectorTransport, transports.DatagramTranspor
self._sock.sendto(data, addr)
except (BlockingIOError, InterruptedError):
self._buffer.appendleft((data, addr)) # Try again later.
self._buffer_size += len(data)
self._buffer_size += len(data) + self._header_size
break
except OSError as exc:
self._protocol.error_received(exc)

View file

@ -566,6 +566,8 @@ class ProactorDatagramTransportTests(test_utils.TestCase):
self.assertTrue(self.proactor.sendto.called)
self.proactor.sendto.assert_called_with(
self.sock, data, addr=('0.0.0.0', 1234))
self.assertFalse(transport._buffer)
self.assertEqual(0, transport._buffer_size)
def test_sendto_bytearray(self):
data = bytearray(b'data')

View file

@ -1460,6 +1460,47 @@ class SelectorDatagramTransportTests(test_utils.TestCase):
transport.sendto(b'data', (1,))
self.assertEqual(transport._conn_lost, 2)
def test_sendto_sendto_ready(self):
data = b'data'
# First queue up the buffer by having the socket blocked
self.sock.sendto.side_effect = BlockingIOError
transport = self.datagram_transport()
transport.sendto(data, ('0.0.0.0', 12345))
self.loop.assert_writer(7, transport._sendto_ready)
self.assertEqual(1, len(transport._buffer))
self.assertEqual(transport._buffer_size, len(data) + transport._header_size)
# Now let the socket send the buffer
self.sock.sendto.side_effect = None
transport._sendto_ready()
self.assertTrue(self.sock.sendto.called)
self.assertEqual(
self.sock.sendto.call_args[0], (data, ('0.0.0.0', 12345)))
self.assertFalse(self.loop.writers)
self.assertFalse(transport._buffer)
self.assertEqual(transport._buffer_size, 0)
def test_sendto_sendto_ready_blocked(self):
data = b'data'
# First queue up the buffer by having the socket blocked
self.sock.sendto.side_effect = BlockingIOError
transport = self.datagram_transport()
transport.sendto(data, ('0.0.0.0', 12345))
self.loop.assert_writer(7, transport._sendto_ready)
self.assertEqual(1, len(transport._buffer))
self.assertEqual(transport._buffer_size, len(data) + transport._header_size)
# Now try to send the buffer, it will be added to buffer again if it fails
transport._sendto_ready()
self.assertTrue(self.sock.sendto.called)
self.assertEqual(
self.sock.sendto.call_args[0], (data, ('0.0.0.0', 12345)))
self.assertTrue(self.loop.writers)
self.assertEqual(1, len(transport._buffer))
self.assertEqual(transport._buffer_size, len(data) + transport._header_size)
def test_sendto_ready(self):
data = b'data'
self.sock.sendto.return_value = len(data)

View file

@ -0,0 +1,2 @@
Fix :meth:`asyncio.DatagramTransport.sendto` to account for datagram header size when
data cannot be sent.