mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Back the UDP resend timer off while acks come back untimed
On a slow, congested link every message was resent before its ack came back, so no round trip could be timed and the timer stayed at 0.5 s: 16 datagrams per message. Eight progress acks in a row with no timed round trip now double the timer, up to its 2 s cap, until a sample resets it. Random loss still yields samples, so it never triggers there.
This commit is contained in:
+21
-3
@@ -142,6 +142,9 @@ UDP_RESEND_MIN = 0.2
|
||||
UDP_RESEND_MAX = 2.0
|
||||
# Acks still asking for the head after later messages arrived; resend it once
|
||||
UDP_DUP_ACKS = 2
|
||||
# Progress acks in a row with no round trip to time: the link is slower than
|
||||
# the resend timer, so back it off (random loss still yields samples)
|
||||
UDP_UNTIMED_ACKS = 8
|
||||
# Refusals in a row with no ACK between: the device's UDP port is closed
|
||||
UDP_REFUSALS = 3
|
||||
UDP_FALLBACK = b"\x00"
|
||||
@@ -616,6 +619,10 @@ class UdpChannel:
|
||||
self._committed = False
|
||||
self._srtt: float | None = None
|
||||
self._rttvar = 0.0
|
||||
# Doubles while acks come back untimed, so a link slower than the
|
||||
# timer still yields messages sent once to time (Karn, RFC 6298)
|
||||
self._backoff = 1
|
||||
self._untimed = 0
|
||||
self._dup_acks = 0
|
||||
self._free = UDP_WINDOW # the device's empty slots, as of its last ACK
|
||||
self._last_ping = 0.0
|
||||
@@ -683,8 +690,10 @@ class UdpChannel:
|
||||
|
||||
def _rto(self) -> float:
|
||||
if self._srtt is None:
|
||||
return UDP_RESEND_INITIAL
|
||||
return min(max(self._srtt + 4 * self._rttvar, UDP_RESEND_MIN), UDP_RESEND_MAX)
|
||||
rto = UDP_RESEND_INITIAL
|
||||
else:
|
||||
rto = max(self._srtt + 4 * self._rttvar, UDP_RESEND_MIN)
|
||||
return min(rto * self._backoff, UDP_RESEND_MAX)
|
||||
|
||||
def _send(self, msg_type: int, seq: int, payload: bytes) -> None:
|
||||
flags = UDP_FLAG_COMMITTED if self._committed else 0
|
||||
@@ -754,6 +763,8 @@ class UdpChannel:
|
||||
|
||||
def _sample_rtt(self, sample: float) -> None:
|
||||
# RFC 6298 smoothing
|
||||
self._backoff = 1
|
||||
self._untimed = 0
|
||||
if self._srtt is None:
|
||||
self._srtt, self._rttvar = sample, sample / 2
|
||||
else:
|
||||
@@ -777,13 +788,20 @@ class UdpChannel:
|
||||
head = self._queue[0].seq
|
||||
index = (prompted - head) & 0xFFFF
|
||||
# This datagram's own round trip; only from messages sent once (Karn)
|
||||
if index < len(self._queue) and self._queue[index].sends == 1:
|
||||
timed = index < len(self._queue) and self._queue[index].sends == 1
|
||||
if timed:
|
||||
self._sample_rtt(now - self._queue[index].last_send)
|
||||
done = (received - head) & 0xFFFF
|
||||
if 0 < done <= len(self._queue):
|
||||
del self._queue[:done]
|
||||
self._last_progress = now
|
||||
self._dup_acks = 0
|
||||
if not timed:
|
||||
self._untimed += 1
|
||||
if self._untimed >= UDP_UNTIMED_ACKS:
|
||||
self._untimed = 0
|
||||
if self._rto() < UDP_RESEND_MAX:
|
||||
self._backoff *= 2
|
||||
elif done == 0 and self._queue[0].sends:
|
||||
# A later message arrived while the head is missing; resend it once
|
||||
self._dup_acks += 1
|
||||
|
||||
@@ -1958,3 +1958,31 @@ def test_start_udp_socket_error_falls_back(mock_socket: Mock) -> None:
|
||||
call(espota2.UDP_REQUEST),
|
||||
call(espota2.UDP_FALLBACK),
|
||||
]
|
||||
|
||||
|
||||
def test_udp_channel_backs_off_when_acks_come_back_untimed() -> None:
|
||||
"""Progress acks that only answer resent messages mean the link is slower
|
||||
than the timer: it backs off, up to its cap, until a sample resets it."""
|
||||
token = b"\x01\x01\x01\x01"
|
||||
channel = espota2.UdpChannel(Mock(), token)
|
||||
channel._queue = [espota2._UdpMessage(seq, b"x") for seq in range(40)]
|
||||
rtos = []
|
||||
for received in range(1, 33):
|
||||
for message in channel._queue[:1]:
|
||||
message.sends = 2 # every message was resent before its ack
|
||||
rtos.append(channel._rto())
|
||||
ack = (
|
||||
bytes([espota2.UDP_MSG_ACK])
|
||||
+ token
|
||||
+ received.to_bytes(2, "big")
|
||||
+ (received - 1).to_bytes(2, "big")
|
||||
+ bytes(2)
|
||||
+ b"\x40"
|
||||
)
|
||||
channel._handle_ack(ack)
|
||||
n = espota2.UDP_UNTIMED_ACKS
|
||||
assert rtos[0] == espota2.UDP_RESEND_INITIAL
|
||||
assert rtos[n] == espota2.UDP_RESEND_INITIAL * 2
|
||||
assert rtos[-1] == espota2.UDP_RESEND_MAX
|
||||
channel._sample_rtt(0.1)
|
||||
assert channel._rto() == pytest.approx(0.3) # srtt 0.1 + 4 * rttvar 0.05
|
||||
|
||||
Reference in New Issue
Block a user