%PDF- %PDF-
Direktori : /backups/router/usr/local/lib/python3.11/site-packages/aioquic/quic/ |
Current File : //backups/router/usr/local/lib/python3.11/site-packages/aioquic/quic/recovery.py |
import logging import math from typing import Any, Callable, Dict, Iterable, List, Optional from .congestion import cubic, reno # noqa from .congestion.base import K_GRANULARITY, create_congestion_control from .logger import QuicLoggerTrace from .packet_builder import QuicDeliveryState, QuicSentPacket from .rangeset import RangeSet # loss detection K_PACKET_THRESHOLD = 3 K_TIME_THRESHOLD = 9 / 8 K_MICRO_SECOND = 0.000001 K_SECOND = 1.0 class QuicPacketSpace: def __init__(self) -> None: self.ack_at: Optional[float] = None self.ack_queue = RangeSet() self.discarded = False self.expected_packet_number = 0 self.largest_received_packet = -1 self.largest_received_time: Optional[float] = None # sent packets and loss self.ack_eliciting_in_flight = 0 self.largest_acked_packet = 0 self.loss_time: Optional[float] = None self.sent_packets: Dict[int, QuicSentPacket] = {} class QuicPacketPacer: def __init__(self, *, max_datagram_size: int) -> None: self._max_datagram_size = max_datagram_size self.bucket_max: float = 0.0 self.bucket_time: float = 0.0 self.evaluation_time: float = 0.0 self.packet_time: Optional[float] = None def next_send_time(self, now: float) -> float: if self.packet_time is not None: self.update_bucket(now=now) if self.bucket_time <= 0: return now + self.packet_time return None def update_after_send(self, now: float) -> None: if self.packet_time is not None: self.update_bucket(now=now) if self.bucket_time < self.packet_time: self.bucket_time = 0.0 else: self.bucket_time -= self.packet_time def update_bucket(self, now: float) -> None: if now > self.evaluation_time: self.bucket_time = min( self.bucket_time + (now - self.evaluation_time), self.bucket_max ) self.evaluation_time = now def update_rate(self, congestion_window: int, smoothed_rtt: float) -> None: pacing_rate = congestion_window / max(smoothed_rtt, K_MICRO_SECOND) self.packet_time = max( K_MICRO_SECOND, min(self._max_datagram_size / pacing_rate, K_SECOND) ) self.bucket_max = ( max( 2 * self._max_datagram_size, min(congestion_window // 4, 16 * self._max_datagram_size), ) / pacing_rate ) if self.bucket_time > self.bucket_max: self.bucket_time = self.bucket_max class QuicPacketRecovery: """ Packet loss and congestion controller. """ def __init__( self, *, congestion_control_algorithm: str, initial_rtt: float, max_datagram_size: int, peer_completed_address_validation: bool, send_probe: Callable[[], None], logger: Optional[logging.LoggerAdapter] = None, quic_logger: Optional[QuicLoggerTrace] = None, ) -> None: self.max_ack_delay = 0.025 self.peer_completed_address_validation = peer_completed_address_validation self.spaces: List[QuicPacketSpace] = [] # callbacks self._logger = logger self._quic_logger = quic_logger self._send_probe = send_probe # loss detection self._pto_count = 0 self._rtt_initial = initial_rtt self._rtt_initialized = False self._rtt_latest = 0.0 self._rtt_min = math.inf self._rtt_smoothed = 0.0 self._rtt_variance = 0.0 self._time_of_last_sent_ack_eliciting_packet = 0.0 # congestion control self._cc = create_congestion_control( congestion_control_algorithm, max_datagram_size=max_datagram_size ) self._pacer = QuicPacketPacer(max_datagram_size=max_datagram_size) @property def bytes_in_flight(self) -> int: return self._cc.bytes_in_flight @property def congestion_window(self) -> int: return self._cc.congestion_window def discard_space(self, space: QuicPacketSpace) -> None: assert space in self.spaces self._cc.on_packets_expired( packets=filter(lambda x: x.in_flight, space.sent_packets.values()) ) space.sent_packets.clear() space.ack_at = None space.ack_eliciting_in_flight = 0 space.loss_time = None # reset PTO count self._pto_count = 0 if self._quic_logger is not None: self._log_metrics_updated() def get_loss_detection_time(self) -> float: # loss timer loss_space = self._get_loss_space() if loss_space is not None: return loss_space.loss_time # packet timer if ( not self.peer_completed_address_validation or sum(space.ack_eliciting_in_flight for space in self.spaces) > 0 ): timeout = self.get_probe_timeout() * (2**self._pto_count) return self._time_of_last_sent_ack_eliciting_packet + timeout return None def get_probe_timeout(self) -> float: if not self._rtt_initialized: return 2 * self._rtt_initial return ( self._rtt_smoothed + max(4 * self._rtt_variance, K_GRANULARITY) + self.max_ack_delay ) def on_ack_received( self, *, ack_rangeset: RangeSet, ack_delay: float, now: float, space: QuicPacketSpace, ) -> None: """ Update metrics as the result of an ACK being received. """ is_ack_eliciting = False largest_acked = ack_rangeset.bounds().stop - 1 largest_newly_acked = None largest_sent_time = None if largest_acked > space.largest_acked_packet: space.largest_acked_packet = largest_acked for packet_number in sorted(space.sent_packets.keys()): if packet_number > largest_acked: break if packet_number in ack_rangeset: # remove packet and update counters packet = space.sent_packets.pop(packet_number) if packet.is_ack_eliciting: is_ack_eliciting = True space.ack_eliciting_in_flight -= 1 if packet.in_flight: self._cc.on_packet_acked(packet=packet, now=now) largest_newly_acked = packet_number largest_sent_time = packet.sent_time # trigger callbacks for handler, args in packet.delivery_handlers: handler(QuicDeliveryState.ACKED, *args) # nothing to do if there are no newly acked packets if largest_newly_acked is None: return if largest_acked == largest_newly_acked and is_ack_eliciting: latest_rtt = now - largest_sent_time log_rtt = True # limit ACK delay to max_ack_delay ack_delay = min(ack_delay, self.max_ack_delay) # update RTT estimate, which cannot be < 1 ms self._rtt_latest = max(latest_rtt, 0.001) if self._rtt_latest < self._rtt_min: self._rtt_min = self._rtt_latest if self._rtt_latest > self._rtt_min + ack_delay: self._rtt_latest -= ack_delay if not self._rtt_initialized: self._rtt_initialized = True self._rtt_variance = latest_rtt / 2 self._rtt_smoothed = latest_rtt else: self._rtt_variance = 3 / 4 * self._rtt_variance + 1 / 4 * abs( self._rtt_min - self._rtt_latest ) self._rtt_smoothed = ( 7 / 8 * self._rtt_smoothed + 1 / 8 * self._rtt_latest ) # inform congestion controller self._cc.on_rtt_measurement(now=now, rtt=latest_rtt) self._pacer.update_rate( congestion_window=self._cc.congestion_window, smoothed_rtt=self._rtt_smoothed, ) else: log_rtt = False self._detect_loss(now=now, space=space) # reset PTO count self._pto_count = 0 if self._quic_logger is not None: self._log_metrics_updated(log_rtt=log_rtt) def on_loss_detection_timeout(self, *, now: float) -> None: loss_space = self._get_loss_space() if loss_space is not None: self._detect_loss(now=now, space=loss_space) else: self._pto_count += 1 self.reschedule_data(now=now) def on_packet_sent(self, *, packet: QuicSentPacket, space: QuicPacketSpace) -> None: space.sent_packets[packet.packet_number] = packet if packet.is_ack_eliciting: space.ack_eliciting_in_flight += 1 if packet.in_flight: if packet.is_ack_eliciting: self._time_of_last_sent_ack_eliciting_packet = packet.sent_time # add packet to bytes in flight self._cc.on_packet_sent(packet=packet) if self._quic_logger is not None: self._log_metrics_updated() def reschedule_data(self, *, now: float) -> None: """ Schedule some data for retransmission. """ # if there is any outstanding CRYPTO, retransmit it crypto_scheduled = False for space in self.spaces: packets = tuple( filter(lambda i: i.is_crypto_packet, space.sent_packets.values()) ) if packets: self._on_packets_lost(now=now, packets=packets, space=space) crypto_scheduled = True if crypto_scheduled and self._logger is not None: self._logger.debug("Scheduled CRYPTO data for retransmission") # ensure an ACK-elliciting packet is sent self._send_probe() def _detect_loss(self, *, now: float, space: QuicPacketSpace) -> None: """ Check whether any packets should be declared lost. """ loss_delay = K_TIME_THRESHOLD * ( max(self._rtt_latest, self._rtt_smoothed) if self._rtt_initialized else self._rtt_initial ) packet_threshold = space.largest_acked_packet - K_PACKET_THRESHOLD time_threshold = now - loss_delay lost_packets = [] space.loss_time = None for packet_number, packet in space.sent_packets.items(): if packet_number > space.largest_acked_packet: break if packet_number <= packet_threshold or packet.sent_time <= time_threshold: lost_packets.append(packet) else: packet_loss_time = packet.sent_time + loss_delay if space.loss_time is None or space.loss_time > packet_loss_time: space.loss_time = packet_loss_time self._on_packets_lost(now=now, packets=lost_packets, space=space) def _get_loss_space(self) -> Optional[QuicPacketSpace]: loss_space = None for space in self.spaces: if space.loss_time is not None and ( loss_space is None or space.loss_time < loss_space.loss_time ): loss_space = space return loss_space def _log_metrics_updated(self, log_rtt=False) -> None: data: Dict[str, Any] = self._cc.get_log_data() if log_rtt: data.update( { "latest_rtt": self._quic_logger.encode_time(self._rtt_latest), "min_rtt": self._quic_logger.encode_time(self._rtt_min), "smoothed_rtt": self._quic_logger.encode_time(self._rtt_smoothed), "rtt_variance": self._quic_logger.encode_time(self._rtt_variance), } ) self._quic_logger.log_event( category="recovery", event="metrics_updated", data=data ) def _on_packets_lost( self, *, now: float, packets: Iterable[QuicSentPacket], space: QuicPacketSpace ) -> None: lost_packets_cc = [] for packet in packets: del space.sent_packets[packet.packet_number] if packet.in_flight: lost_packets_cc.append(packet) if packet.is_ack_eliciting: space.ack_eliciting_in_flight -= 1 if self._quic_logger is not None: self._quic_logger.log_event( category="recovery", event="packet_lost", data={ "type": self._quic_logger.packet_type(packet.packet_type), "packet_number": packet.packet_number, }, ) self._log_metrics_updated() # trigger callbacks for handler, args in packet.delivery_handlers: handler(QuicDeliveryState.LOST, *args) # inform congestion controller if lost_packets_cc: self._cc.on_packets_lost(now=now, packets=lost_packets_cc) self._pacer.update_rate( congestion_window=self._cc.congestion_window, smoothed_rtt=self._rtt_smoothed, ) if self._quic_logger is not None: self._log_metrics_updated()