quic_loss (quic v3.0.0)
View SourceQUIC 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
Whether a packet was sent after the first RTT sample, so its loss can count towards persistent congestion (RFC 9002 Section 7.6.2).
The application-space PTO without backoff: how long an ACK for a packet sent now should take on this path.
Get bytes currently in flight.
A congestion signal that is not a loss, an ECN-CE mark, makes the space's current loss episode real: no late acknowledgement undoes its window cut.
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.
The in-flight bytes among application Packets that went out on an earlier path.
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 PTO of a new path whose RTT is not known yet, from InitialRTT.
Carry the PTO backoff of one loss state over to a later one, for an acknowledgement that must not reset it.
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.
Rewrite the frames of the packets in flight in Space. Sizes, flags and bytes in flight stay as they are.
The peer's max_ack_delay (ms) the PTO and RTT samples use.
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} AckMeta is a map containing: - acked_bytes: total bytes from in-flight packets that were acknowledged - largest_acked_sent_time: sent_time of the largest in-flight packet acknowledged - has_in_flight: whether any in-flight packet was acknowledged - late_acked: the repairable frames of each packet acknowledged after it was declared lost, oldest first (RFC 9000 Section 13.3)
Whether an application packet went out on an earlier path.
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. The packet is in flight when it is ack-eliciting.
Record a sent packet, InFlight saying whether it counts towards bytes in flight: ack-eliciting or carrying PADDING (RFC 9002 Section 2 and Appendix A.5).
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.
Persistent congestion has been declared: the losses behind it start no other period, and min_rtt restarts from the newest sample (RFC 9002 Section 5.2).
Handle PTO expiration.
Whether the packets just declared lost in Space establish persistent congestion (RFC 9002 Section 7.6.2). The two packets need not be declared lost together: the space's earlier losses count too. A packet number neither lost nor queued counts as acknowledged; another space blocks every packet sent before the newest one it acknowledged.
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.
The packet threshold loss detection uses in a space, raised by reordering it has seen (RFC 9002 Section 6.1.1).
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.
The frames of a lost packet whose information is sent again (RFC 9000 Section 13.3).
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.
Use MaxAckDelay (ms) for the peer's max_ack_delay, as an ACK_FREQUENCY request changes it (draft-ietf-quic-ack-frequency-14 Section 7).
Adopt the peer's ack_delay_exponent and max_ack_delay (ms), from its transport parameters.
Update RTT estimates with a new sample.
Types
Functions
-spec after_first_rtt_sample(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()]}) -> boolean().
Whether a packet was sent after the first RTT sample, so its loss can count towards persistent congestion (RFC 9002 Section 7.6.2).
-spec base_pto(loss_state()) -> non_neg_integer().
The application-space PTO without backoff: how long an ACK for a packet sent now should take on this path.
-spec bytes_in_flight(loss_state()) -> non_neg_integer().
Get bytes currently in flight.
-spec confirm_losses(space(), loss_state()) -> loss_state().
A congestion signal that is not a loss, an ECN-CE mark, makes the space's current loss episode real: no late acknowledgement undoes its window cut.
-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.
-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.
-spec earlier_path_bytes(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()]}]) -> non_neg_integer().
The in-flight bytes among application Packets that went out on an earlier path.
-spec get_loss_time_and_space(loss_state()) -> {non_neg_integer(), space()} | none.
Get the loss time for setting timers.
-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).
-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.
-spec handshake_confirmed(loss_state()) -> boolean().
Whether the handshake has been confirmed (RFC 9001 Section 4.1.2).
-spec initial_path_pto(loss_state(), non_neg_integer()) -> non_neg_integer().
The PTO of a new path whose RTT is not known yet, from InitialRTT.
-spec keep_pto_backoff(loss_state(), loss_state()) -> loss_state().
Carry the PTO backoff of one loss state over to a later one, for an acknowledgement that must not reset it.
-spec largest_acked(loss_state(), space()) -> non_neg_integer().
The highest packet number acknowledged in a space, or 0 before anything has been.
-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.
-spec map_frames(space(), fun(([term()]) -> [term()]), loss_state()) -> loss_state().
Rewrite the frames of the packets in flight in Space. Sizes, flags and bytes in flight stay as they are.
-spec max_ack_delay(loss_state()) -> non_neg_integer().
The peer's max_ack_delay (ms) the PTO and RTT samples use.
-spec new() -> loss_state().
Create a new loss detection state.
-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)
-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.
-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()}.
Process an ACK frame. Returns {NewState, AckedPackets, LostPackets, AckMeta} AckMeta is a map containing: - acked_bytes: total bytes from in-flight packets that were acknowledged - largest_acked_sent_time: sent_time of the largest in-flight packet acknowledged - has_in_flight: whether any in-flight packet was acknowledged - late_acked: the repairable frames of each packet acknowledged after it was declared lost, oldest first (RFC 9000 Section 13.3)
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.
-spec on_earlier_path(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()]}) -> boolean().
Whether an application packet went out on an earlier path.
-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.
-spec on_packet_sent(space(), loss_state(), non_neg_integer(), non_neg_integer(), boolean()) -> loss_state().
Record that a packet was sent (without 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.
-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. The packet is in flight when it is ack-eliciting.
-spec on_packet_sent(space(), loss_state(), non_neg_integer(), non_neg_integer(), boolean(), boolean(), [term()], integer()) -> loss_state().
Record a sent packet, InFlight saying whether it counts towards bytes in flight: ack-eliciting or carrying PADDING (RFC 9002 Section 2 and Appendix A.5).
-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.
-spec on_persistent_congestion(loss_state()) -> loss_state().
Persistent congestion has been declared: the losses behind it start no other period, and min_rtt restarts from the newest sample (RFC 9002 Section 5.2).
-spec on_pto_expired(loss_state()) -> loss_state().
Handle PTO expiration.
-spec persistent_congestion(space(), [#sent_packet{pn :: non_neg_integer(), time_sent :: non_neg_integer(), ack_eliciting :: boolean(), in_flight :: boolean(), size :: non_neg_integer(), frames :: [term()]}], loss_state()) -> boolean().
Whether the packets just declared lost in Space establish persistent congestion (RFC 9002 Section 7.6.2). The two packets need not be declared lost together: the space's earlier losses count too. A packet number neither lost nor queued counts as acknowledged; another space blocks every packet sent before the newest one it acknowledged.
-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.
-spec pto_count(loss_state()) -> non_neg_integer().
Get current PTO count.
-spec reorder_threshold(space(), loss_state()) -> pos_integer().
The packet threshold loss detection uses in a space, raised by reordering it has seen (RFC 9002 Section 6.1.1).
-spec reset_for_new_path(loss_state() | undefined) -> loss_state().
-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.
The frames of a lost packet whose information is sent again (RFC 9000 Section 13.3).
-spec rtt(loss_state()) -> quic_rtt:state().
The path's RTT estimate, read through quic_rtt.
-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.
-spec set_max_ack_delay(loss_state(), non_neg_integer()) -> loss_state().
Use MaxAckDelay (ms) for the peer's max_ack_delay, as an ACK_FREQUENCY request changes it (draft-ietf-quic-ack-frequency-14 Section 7).
-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.
-spec stream_has_unacked_below(loss_state(), non_neg_integer(), non_neg_integer()) -> boolean().
-spec update_rtt(loss_state(), non_neg_integer(), non_neg_integer()) -> loss_state().
Update RTT estimates with a new sample.