quic_loss (quic v1.3.0)

View Source

QUIC loss detection implementation.

This module implements: - Packet loss detection using time and packet thresholds - RTT estimation (smoothed RTT, RTT variance) - Probe Timeout (PTO) calculation - Loss detection timer management

Loss Detection Methods

1. Packet Threshold: A packet is lost if a packet sent more than kPacketThreshold (3) later has been acknowledged.

2. Time Threshold: A packet is lost if it was sent more than max(kTimeThreshold * smoothed_rtt, kGranularity) ago and a later packet has been acknowledged.

Summary

Functions

Get bytes currently in flight.

Detect lost packets based on time and packet thresholds. Scans the sent queue head-to-tail (oldest first) and splits into {Lost, Surviving}. Returns the new loss_state and the lost packets.

Get the loss time for setting timers. The queue is oldest-first, so the earliest in_flight packet is at the head; this turns the previous O(n) map fold into an O(1) head peek in the common case (head is in_flight).

Calculate the Probe Timeout. PTO = smoothed_rtt + max(4 * rttvar, kGranularity) + max_ack_delay

Check if we have received a real RTT sample. Returns false until the first ACK provides a real RTT measurement.

Get the latest RTT sample.

Get the minimum RTT.

Create a new loss detection state.

Create a new loss detection state with options. Options: - max_ack_delay: Maximum ACK delay (default: 25ms) - initial_rtt: Initial RTT estimate in ms (default: 100ms)

Get the oldest unacked packet (for PTO probe selection). Returns {ok, #sent_packet{}} or none. Head of the sent queue is by construction the oldest in-flight packet.

Process an ACK frame. Returns {NewState, AckedPackets, LostPackets, AckMeta} or {error, ack_range_too_large} AckMeta is a map containing: - acked_bytes: total bytes from ack-eliciting packets that were acknowledged - largest_ae_time: sent_time of the largest ack-eliciting packet acknowledged

Record that a packet was sent (without frames).

Record that a packet was sent with frames. Samples the send time itself. Callers that already hold a Now should use on_packet_sent/6 to avoid a duplicate monotonic_time/1 BIF call.

Like on_packet_sent/5 but uses the caller-supplied monotonic millisecond timestamp. The connection send loop reuses one Now per packet for both loss tracking and last_activity, saving a BIF call.

Handle PTO expiration.

Get current PTO count.

Filter frames to get only retransmittable ones. Per RFC 9002, PADDING, ACK, and CONNECTION_CLOSE frames are not retransmitted.

Get the RTT variance.

Get all sent packets. Returned as a map for API compatibility. Built on demand from the queue; intended for tests and diagnostics, not the hot path.

Get the smoothed RTT.

Update RTT estimates with a new sample.

Types

loss_state/0

-opaque loss_state()

Functions

bytes_in_flight(Loss_state)

-spec bytes_in_flight(loss_state()) -> non_neg_integer().

Get bytes currently in flight.

detect_lost_packets(Loss_state, LargestAcked)

-spec detect_lost_packets(loss_state(), non_neg_integer()) ->
                             {loss_state(),
                              [#sent_packet{pn :: non_neg_integer(),
                                            time_sent :: non_neg_integer(),
                                            ack_eliciting :: boolean(),
                                            in_flight :: boolean(),
                                            size :: non_neg_integer(),
                                            frames :: [term()]}]}.

Detect lost packets based on time and packet thresholds. Scans the sent queue head-to-tail (oldest first) and splits into {Lost, Surviving}. Returns the new loss_state and the lost packets.

get_loss_time_and_space(Loss_state)

-spec get_loss_time_and_space(loss_state()) -> {non_neg_integer() | undefined, atom()}.

Get the loss time for setting timers. The queue is oldest-first, so the earliest in_flight packet is at the head; this turns the previous O(n) map fold into an O(1) head peek in the common case (head is in_flight).

get_pto(Loss_state)

-spec get_pto(loss_state()) -> non_neg_integer().

Calculate the Probe Timeout. PTO = smoothed_rtt + max(4 * rttvar, kGranularity) + max_ack_delay

has_rtt_sample(Loss_state)

-spec has_rtt_sample(loss_state()) -> boolean().

Check if we have received a real RTT sample. Returns false until the first ACK provides a real RTT measurement.

latest_rtt(Loss_state)

-spec latest_rtt(loss_state()) -> non_neg_integer().

Get the latest RTT sample.

min_rtt(Loss_state)

-spec min_rtt(loss_state()) -> non_neg_integer() | infinity.

Get the minimum RTT.

new()

-spec new() -> loss_state().

Create a new loss detection state.

new(Opts)

-spec new(map()) -> loss_state().

Create a new loss detection state with options. Options: - max_ack_delay: Maximum ACK delay (default: 25ms) - initial_rtt: Initial RTT estimate in ms (default: 100ms)

oldest_unacked(Loss_state)

-spec oldest_unacked(loss_state()) ->
                        {ok,
                         #sent_packet{pn :: non_neg_integer(),
                                      time_sent :: non_neg_integer(),
                                      ack_eliciting :: boolean(),
                                      in_flight :: boolean(),
                                      size :: non_neg_integer(),
                                      frames :: [term()]}} |
                        none.

Get the oldest unacked packet (for PTO probe selection). Returns {ok, #sent_packet{}} or none. Head of the sent queue is by construction the oldest in-flight packet.

on_ack_received(State, _, Now)

-spec on_ack_received(loss_state(), term(), non_neg_integer()) ->
                         {loss_state(),
                          [#sent_packet{pn :: non_neg_integer(),
                                        time_sent :: non_neg_integer(),
                                        ack_eliciting :: boolean(),
                                        in_flight :: boolean(),
                                        size :: non_neg_integer(),
                                        frames :: [term()]}],
                          [#sent_packet{pn :: non_neg_integer(),
                                        time_sent :: non_neg_integer(),
                                        ack_eliciting :: boolean(),
                                        in_flight :: boolean(),
                                        size :: non_neg_integer(),
                                        frames :: [term()]}],
                          map()} |
                         {error, ack_range_too_large}.

Process an ACK frame. Returns {NewState, AckedPackets, LostPackets, AckMeta} or {error, ack_range_too_large} AckMeta is a map containing: - acked_bytes: total bytes from ack-eliciting packets that were acknowledged - largest_ae_time: sent_time of the largest ack-eliciting packet acknowledged

Implementation: three passes over the sent queue. 1. classify_ack_q: split queue into (acked, kept-unacked) by the ACK ranges in a single head-to-tail walk. Stops early once we pass LargestAcked. 2. maybe_update_rtt: RTT sample derived from the largest acked ack-eliciting packet, if present. 3. detect_lost_q: over the kept survivors, apply packet-threshold and time-threshold loss criteria using the freshly updated SRTT.

on_packet_sent(State, PacketNumber, Size, AckEliciting)

-spec on_packet_sent(loss_state(), non_neg_integer(), non_neg_integer(), boolean()) -> loss_state().

Record that a packet was sent (without frames).

on_packet_sent(State, PacketNumber, Size, AckEliciting, Frames)

-spec on_packet_sent(loss_state(), non_neg_integer(), non_neg_integer(), boolean(), [term()]) ->
                        loss_state().

Record that a packet was sent with frames. Samples the send time itself. Callers that already hold a Now should use on_packet_sent/6 to avoid a duplicate monotonic_time/1 BIF call.

on_packet_sent(Loss_state, PacketNumber, Size, AckEliciting, Frames, Now)

-spec on_packet_sent(loss_state(), non_neg_integer(), non_neg_integer(), boolean(), [term()], integer()) ->
                        loss_state().

Like on_packet_sent/5 but uses the caller-supplied monotonic millisecond timestamp. The connection send loop reuses one Now per packet for both loss tracking and last_activity, saving a BIF call.

on_pto_expired(Loss_state)

-spec on_pto_expired(loss_state()) -> loss_state().

Handle PTO expiration.

pto_count(Loss_state)

-spec pto_count(loss_state()) -> non_neg_integer().

Get current PTO count.

retransmittable_frames(Frames)

-spec retransmittable_frames([term()]) -> [term()].

Filter frames to get only retransmittable ones. Per RFC 9002, PADDING, ACK, and CONNECTION_CLOSE frames are not retransmitted.

rtt_var(Loss_state)

-spec rtt_var(loss_state()) -> non_neg_integer().

Get the RTT variance.

sent_packets(Loss_state)

-spec sent_packets(loss_state()) ->
                      #{non_neg_integer() =>
                            #sent_packet{pn :: non_neg_integer(),
                                         time_sent :: non_neg_integer(),
                                         ack_eliciting :: boolean(),
                                         in_flight :: boolean(),
                                         size :: non_neg_integer(),
                                         frames :: [term()]}}.

Get all sent packets. Returned as a map for API compatibility. Built on demand from the queue; intended for tests and diagnostics, not the hot path.

smoothed_rtt(Loss_state)

-spec smoothed_rtt(loss_state()) -> non_neg_integer().

Get the smoothed RTT.

update_rtt(Loss_state, LatestRTT, AckDelay)

-spec update_rtt(loss_state(), non_neg_integer(), non_neg_integer()) -> loss_state().

Update RTT estimates with a new sample.