quic_loss (quic v2.0.3)

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.

Drop a packet number space whose keys are gone (RFC 9002 Appendix A.11). Returns the bytes removed so the caller can subtract the same amount from congestion control, which keeps its own count.

Get the loss time for setting timers.

The PTO for one packet number space. max_ack_delay applies only to Application Data: the peer is expected not to delay Initial or Handshake acknowledgements (RFC 9002 Section 6.2.1).

RFC 9002 Appendix A.8 GetPtoTimeAndSpace: the earliest probe deadline and the space it belongs to, as an absolute monotonic millisecond time, or none when no probe should be armed.

Whether the handshake has been confirmed (RFC 9001 Section 4.1.2).

The highest packet number acknowledged in a space, or 0 before anything has been.

Latest sign of forward progress for the disconnect timeout: the last received ACK, or the start of the current outstanding burst, whichever is later. undefined until either has happened.

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

Mark the handshake confirmed (RFC 9001 Section 4.1.2), from which point max_ack_delay caps RTT samples and joins the PTO.

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/7 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.

Batched on_packet_sent for a run of ack-eliciting packets sent at the same instant: one queue fold and one record update. Tracked is [{PN, Size, Frame}] in ascending PN order.

Handle PTO expiration.

The PTO the persistent congestion window is built from (RFC 9002 Section 7.6.1): max_ack_delay whatever the packet number space, and no backoff, so the window does not widen during the blackout it is meant to detect.

Get current PTO count.

Reset recovery state after a Retry (RFC 9002 Section 6.3), and hand back the packets that were in flight so the caller can decide what to resend.

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

The path's RTT estimate, read through quic_rtt.

The packets still unacked in one space, keyed by packet number. Built on demand from the queue; for tests and diagnostics, not the hot path. Keyed per space because packet numbers restart in each one.

Adopt the peer's ack_delay_exponent and max_ack_delay (ms), from its transport parameters.

Update RTT estimates with a new sample.

Types

loss_state/0

-opaque loss_state()

space/0

-type space() :: initial | handshake | app.

Functions

bytes_in_flight(Loss_state)

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

Get bytes currently in flight.

detect_lost_packets(Space, Loss_state, LargestAcked)

-spec detect_lost_packets(space(), 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.

discard_space(Space, Loss_state)

-spec discard_space(space(), loss_state()) -> {loss_state(), non_neg_integer()}.

Drop a packet number space whose keys are gone (RFC 9002 Appendix A.11). Returns the bytes removed so the caller can subtract the same amount from congestion control, which keeps its own count.

Without this the space keeps its packets in flight forever: nothing can acknowledge them, so they hold the connection's in-flight byte count above zero and keep winning the probe selector.

get_loss_time_and_space(Loss_state)

-spec get_loss_time_and_space(loss_state()) -> {non_neg_integer(), space()} | none.

Get the loss time for setting timers.

get_pto(Loss_state, Space)

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

The PTO for one packet number space. max_ack_delay applies only to Application Data: the peer is expected not to delay Initial or Handshake acknowledgements (RFC 9002 Section 6.2.1).

get_pto_time_and_space(Loss_state, Now, Handshake_status)

-spec get_pto_time_and_space(loss_state(),
                             integer(),
                             #handshake_status{has_handshake_keys :: boolean(),
                                               peer_completed_address_validation :: boolean()}) ->
                                {integer(), space()} | none.

RFC 9002 Appendix A.8 GetPtoTimeAndSpace: the earliest probe deadline and the space it belongs to, as an absolute monotonic millisecond time, or none when no probe should be armed.

Two rules here carry the weight. With nothing ack-eliciting in flight anywhere the peer cannot have completed address validation, so an anti-deadlock probe is armed from now to keep the client sending; that is the client's job, and a server blocked at its amplification limit is stopped before this by the caller. And Application Data is skipped entirely until the handshake is confirmed, which Section 6.2.1 makes a MUST NOT rather than a preference.

handshake_confirmed(Loss_state)

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

Whether the handshake has been confirmed (RFC 9001 Section 4.1.2).

largest_acked(Loss_state, Space)

-spec largest_acked(loss_state(), space()) -> non_neg_integer().

The highest packet number acknowledged in a space, or 0 before anything has been.

last_progress(Loss_state)

-spec last_progress(loss_state()) -> non_neg_integer() | undefined.

Latest sign of forward progress for the disconnect timeout: the last received ACK, or the start of the current outstanding burst, whichever is later. undefined until either has happened.

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(Space, Loss_state)

-spec oldest_unacked(space(), 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(Space, State, _, Now)

-spec on_ack_received(space(), 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_handshake_confirmed(Loss_state)

-spec on_handshake_confirmed(loss_state()) -> loss_state().

Mark the handshake confirmed (RFC 9001 Section 4.1.2), from which point max_ack_delay caps RTT samples and joins the PTO.

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

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

Record that a packet was sent (without frames).

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

-spec on_packet_sent(space(), 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/7 to avoid a duplicate monotonic_time/1 BIF call.

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

-spec on_packet_sent(space(),
                     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_packets_sent_run(Space, Loss_state, Tracked, Now)

-spec on_packets_sent_run(space(),
                          loss_state(),
                          [{non_neg_integer(), non_neg_integer(), term()}],
                          integer()) ->
                             loss_state().

Batched on_packet_sent for a run of ack-eliciting packets sent at the same instant: one queue fold and one record update. Tracked is [{PN, Size, Frame}] in ascending PN order.

on_pto_expired(Loss_state)

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

Handle PTO expiration.

persistent_congestion_pto(Loss_state)

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

The PTO the persistent congestion window is built from (RFC 9002 Section 7.6.1): max_ack_delay whatever the packet number space, and no backoff, so the window does not widen during the blackout it is meant to detect.

pto_count(Loss_state)

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

Get current PTO count.

reset_for_new_path(Loss_state)

-spec reset_for_new_path(loss_state() | undefined) -> loss_state().

reset_for_retry(Loss_state, Now)

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

Reset recovery state after a Retry (RFC 9002 Section 6.3), and hand back the packets that were in flight so the caller can decide what to resend.

They come back keyed by space: #sent_packet{} carries no space and packet numbers restart in each one, so a flat list could not tell an Initial packet from a 0-RTT one. Only the application entry is worth replaying; the Initial flight is rebuilt from the retained TLS state with the Retry token in it, so replaying it from here too would send the ClientHello twice.

The RTT estimate goes back to its default rather than adopting the Initial-to-Retry sample, which RFC 9002 permits but does not require.

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(Loss_state)

-spec rtt(loss_state()) -> quic_rtt:state().

The path's RTT estimate, read through quic_rtt.

sent_packets(Space, Loss_state)

-spec sent_packets(space(), 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()]}}.

The packets still unacked in one space, keyed by packet number. Built on demand from the queue; for tests and diagnostics, not the hot path. Keyed per space because packet numbers restart in each one.

set_peer_ack_params(Loss_state, Exponent, MaxAckDelay)

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

Adopt the peer's ack_delay_exponent and max_ack_delay (ms), from its transport parameters.

stream_has_unacked_below(Loss_state, StreamId, ReliableSize)

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

update_rtt(Loss_state, LatestRTT, AckDelay0)

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

Update RTT estimates with a new sample.