nquic_loss (nquic v1.2.0)
View SourceLoss detection per RFC 9002.
Tracks sent packets per packet number space with shared RTT estimation and congestion control. Uses both packet threshold (3 packets) and time threshold (9/8 of max(smoothed_rtt, latest_rtt)) for loss detection. PTO with exponential backoff triggers probes when no ACKs arrive.
Persistent congestion (RFC 9002 Section 7.6) is declared when a contiguous
run of ack-eliciting packets is lost spanning at least
(SRTT + max(4 * RTTvar, kGranularity) + max_ack_delay) * 3 and both
endpoints were sent after the first RTT sample. On detection the congestion
controller's window collapses to 2 * max_datagram_size.
Summary
Functions
Clear the ecn_socket_dirty flag after the caller has applied the transition.
Discard the Initial and Handshake spaces once the handshake is confirmed.
Same as clear_initial_space/1 for each space.
Discard the Initial space after the key discard of RFC 9001 §4.9.1.
The packets of the space leave bytes_in_flight and the ack-eliciting
count, its loss time is cleared, and the PTO backoff resets
(RFC 9002 §6.4, Appendix A.10).
Run loss detection for Space at Now, as the loss timer of
RFC 9002 Appendix A.10 does. The lost packets leave bytes_in_flight
and go to the congestion controller, with the persistent congestion
check of RFC 9002 §7.6. MaxAckDelayUs is as in on_ack_received/7.
Returns the frames of the lost packets.
Get the frames of the ack-eliciting packets in flight in Space, in packet number order.
Get the frames of each ack-eliciting packet in flight in Space, one list
per packet, oldest packet first.
Get the cached count of ack-eliciting packets currently in flight.
Get the total bytes currently in flight across all packet number spaces.
Get the congestion control algorithm from the current loss state.
Get the current congestion window size in bytes.
Get the largest acknowledged packet number for a given space, or 0 if none.
Get the earliest loss time and its packet number space, or undefined
when no space has one (RFC 9002 Appendix A.8, GetLossTimeAndSpace).
On a tie the earlier space wins, in the order Initial, Handshake,
Application.
Get the base time of the PTO timer (RFC 9002 Appendix A.8), or undefined
when no ack-eliciting packet is in flight. For each space with
ack-eliciting packets in flight, the candidate is the send time of the
newest such packet. The base time is the earliest candidate.
Get the current PTO count.
Calculate the probe timeout (microseconds) with exponential backoff.
MaxAckDelayUs must be in microseconds. Callers that hold the peer's
advertised max_ack_delay (milliseconds, per RFC 9000 S18.2) must
convert: MaxAckDelayMs * 1000.
Get the current RTT statistics as a map.
Get all outstanding sent packet numbers across every packet number space.
Get all currently tracked #sent_packet{} records across every packet
number space. Intended for tests and diagnostics; production code should
prefer the dedicated counters (get_bytes_in_flight/1,
has_ack_eliciting_in_flight/1).
Return true if any ack-eliciting packets are currently in flight.
Return true once an ACK acknowledged a packet sent in Space.
Initialize loss detection state with default RTT and NewReno congestion control.
Initialize loss detection state with the given congestion control algorithm.
Initialize loss detection state.
Opts is a single map carrying both pacer (enabled, factor,
burst_packets) and congestion-control (slow_start) knobs. Callers
that only have pacer config can keep passing a pacer_opts/0 shape;
init/2 deduplicates the two via the structural key set.
Whether Frames holds a frame other than ACK, PADDING or
CONNECTION_CLOSE (RFC 9002 §2).
Check whether ECN is enabled for this connection.
Cheap predicate for the ecn_socket_dirty flag.
Pure record-field read, no allocation. The flush hot path uses this to
short-circuit out of the rare validation-failure transition without
paying for a tuple return.
Process an ACK frame without ECN counts. Same as on_ack_received/7
with undefined counts.
Process an ACK frame, returning updated state plus acked and lost frames.
MaxAckDelayUs is the peer's advertised max_ack_delay in microseconds.
It is used to compute the persistent congestion window (RFC 9002 Section
7.6.1). Pass 0 if it is not yet known.
Record a sent packet in the given packet number space.
Increment the PTO count after a probe timeout fires.
Decide whether the pacer admits a send at Now (microsecond
monotonic). Returns pass to send immediately or {block, NextUs}
when the caller must hold off until NextUs. Disabled or in-slow-
start pacers always pass.
Project the pacer + CC configuration as a map suitable for re-passing
to init/2. Used by code paths that re-initialise loss state on path
/ retry transitions and want to preserve pacing and HyStart++ settings.
Disable the pacer. Used by the caller to opt out at runtime.
Whether the pacer is configured on this connection. The pacer may be
present-and-disengaged if cwnd =< ssthresh (still in slow start) or
if the pacing budget has not been built up yet.
Earliest microsecond (monotonic, may be negative on Erlang's
monotonic clock) at which the pacer permits the next send.
undefined if the pacer has never engaged on this connection.
Project a flat path-stats map for nquic_conn:path_stats/1.
Sums per-PN-space ECN counts (peer-reported CE and total) into single
integers so the consumer does not have to know about packet-number
spaces.
Process ECN counts from a received ACK frame (RFC 9002 §7.1).
Compares the CE count of the peer against the last known baseline. When
the CE count increased, the congestion controller gets a congestion
event for a packet sent at SentTime, the send time of the largest
acknowledged packet (RFC 9002 Appendix B.7). A CE increase for a packet
sent before the current recovery period does not reduce the window
again. When the total of the ECN counts decreased (validation failure),
ECN is disabled for the path.
Reset the PTO count to zero after receiving an ACK.
Enable or disable ECN for this connection.
Set the maximum datagram size used by the congestion controller.
Set the PTO count, the exponent of the PTO backoff.
Types
-type cc_opts() :: #{slow_start => standard | hystart_plus_plus, _ => _}.
-type ecn_counts() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}.
-type loss_state() :: #loss_state{sent_packets :: #{nquic_packet:space() => nquic_pn_buf:buf()}, largest_acked_packet :: #{nquic_packet:space() => nquic_packet_number:t()}, loss_time :: #{nquic_packet:space() => non_neg_integer()}, rtt_state :: nquic_rtt:rtt_state(), cc_state :: dynamic(), bytes_in_flight :: non_neg_integer(), pto_count :: non_neg_integer(), ack_eliciting_in_flight :: non_neg_integer(), first_rtt_sample :: non_neg_integer() | undefined, last_send_time :: non_neg_integer() | undefined, recently_lost :: #{nquic_packet:space() => nquic_pn_buf:buf()}, peer_ecn_ce :: #{nquic_packet:space() => non_neg_integer()}, peer_ecn_total :: #{nquic_packet:space() => non_neg_integer()}, ecn_enabled :: boolean(), ecn_socket_dirty :: boolean(), pacer :: pacer_state()}.
-type pacer_opts() :: #{enabled => boolean(), factor => number(), burst_packets => pos_integer()}.
-type pacer_state() :: #pacer_state{enabled :: boolean(), factor :: number(), burst_packets :: pos_integer(), next_send_time :: undefined | integer()}.
-type path_stats() :: #{srtt_us := non_neg_integer(), rttvar_us := non_neg_integer(), min_rtt_us := non_neg_integer(), latest_rtt_us := non_neg_integer(), cwnd := non_neg_integer(), bytes_in_flight := non_neg_integer(), ssthresh := non_neg_integer() | undefined, mss := pos_integer(), ecn_enabled := boolean(), peer_ecn_ce := non_neg_integer(), peer_ecn_total := non_neg_integer(), pto_count := non_neg_integer()}.
-type sent_packet() :: #sent_packet{packet_number :: nquic_packet_number:t(), time_sent :: non_neg_integer(), size :: non_neg_integer(), ack_eliciting :: boolean(), in_flight :: boolean(), frames :: [nquic_frame:t()]}.
Functions
-spec clear_ecn_socket_dirty(loss_state()) -> loss_state().
Clear the ecn_socket_dirty flag after the caller has applied the transition.
-spec clear_handshake_spaces(loss_state()) -> loss_state().
Discard the Initial and Handshake spaces once the handshake is confirmed.
Same as clear_initial_space/1 for each space.
-spec clear_initial_space(loss_state()) -> loss_state().
Discard the Initial space after the key discard of RFC 9001 §4.9.1.
The packets of the space leave bytes_in_flight and the ack-eliciting
count, its loss time is cleared, and the PTO backoff resets
(RFC 9002 §6.4, Appendix A.10).
-spec detect_loss(loss_state(), nquic_packet:space(), non_neg_integer(), non_neg_integer()) -> {ok, loss_state(), [nquic_frame:t()]}.
Run loss detection for Space at Now, as the loss timer of
RFC 9002 Appendix A.10 does. The lost packets leave bytes_in_flight
and go to the congestion controller, with the persistent congestion
check of RFC 9002 §7.6. MaxAckDelayUs is as in on_ack_received/7.
Returns the frames of the lost packets.
-spec get_ack_eliciting_frames(loss_state(), nquic_packet:space()) -> [nquic_frame:t()].
Get the frames of the ack-eliciting packets in flight in Space, in packet number order.
-spec get_ack_eliciting_frames_by_packet(loss_state(), nquic_packet:space()) -> [[nquic_frame:t()]].
Get the frames of each ack-eliciting packet in flight in Space, one list
per packet, oldest packet first.
-spec get_ack_eliciting_in_flight(loss_state()) -> non_neg_integer().
Get the cached count of ack-eliciting packets currently in flight.
-spec get_bytes_in_flight(loss_state()) -> non_neg_integer().
Get the total bytes currently in flight across all packet number spaces.
-spec get_cc_algorithm(loss_state()) -> atom().
Get the congestion control algorithm from the current loss state.
-spec get_cwnd(loss_state()) -> non_neg_integer().
Get the current congestion window size in bytes.
-spec get_largest_acked(loss_state(), nquic_packet:space()) -> non_neg_integer().
Get the largest acknowledged packet number for a given space, or 0 if none.
-spec get_loss_timer(loss_state()) -> {non_neg_integer(), nquic_packet:space()} | undefined.
Get the earliest loss time and its packet number space, or undefined
when no space has one (RFC 9002 Appendix A.8, GetLossTimeAndSpace).
On a tie the earlier space wins, in the order Initial, Handshake,
Application.
-spec get_pto_base_time(loss_state()) -> non_neg_integer() | undefined.
Get the base time of the PTO timer (RFC 9002 Appendix A.8), or undefined
when no ack-eliciting packet is in flight. For each space with
ack-eliciting packets in flight, the candidate is the send time of the
newest such packet. The base time is the earliest candidate.
-spec get_pto_count(loss_state()) -> non_neg_integer().
Get the current PTO count.
-spec get_pto_timeout(loss_state(), non_neg_integer()) -> non_neg_integer().
Calculate the probe timeout (microseconds) with exponential backoff.
MaxAckDelayUs must be in microseconds. Callers that hold the peer's
advertised max_ack_delay (milliseconds, per RFC 9000 S18.2) must
convert: MaxAckDelayMs * 1000.
-spec get_rtt_stats(loss_state()) -> map().
Get the current RTT statistics as a map.
-spec get_sent_packet_numbers(loss_state()) -> [nquic_packet_number:t()].
Get all outstanding sent packet numbers across every packet number space.
-spec get_sent_packets(loss_state()) -> [sent_packet()].
Get all currently tracked #sent_packet{} records across every packet
number space. Intended for tests and diagnostics; production code should
prefer the dedicated counters (get_bytes_in_flight/1,
has_ack_eliciting_in_flight/1).
-spec has_ack_eliciting_in_flight(loss_state()) -> boolean().
Return true if any ack-eliciting packets are currently in flight.
-spec has_received_ack(loss_state(), nquic_packet:space()) -> boolean().
Return true once an ACK acknowledged a packet sent in Space.
-spec init() -> loss_state().
Initialize loss detection state with default RTT and NewReno congestion control.
-spec init(atom()) -> loss_state().
Initialize loss detection state with the given congestion control algorithm.
-spec init(atom(), pacer_opts() | cc_opts()) -> loss_state().
Initialize loss detection state.
Opts is a single map carrying both pacer (enabled, factor,
burst_packets) and congestion-control (slow_start) knobs. Callers
that only have pacer config can keep passing a pacer_opts/0 shape;
init/2 deduplicates the two via the structural key set.
-spec is_ack_eliciting([nquic_frame:t()]) -> boolean().
Whether Frames holds a frame other than ACK, PADDING or
CONNECTION_CLOSE (RFC 9002 §2).
-spec is_ecn_enabled(loss_state()) -> boolean().
Check whether ECN is enabled for this connection.
-spec is_ecn_socket_dirty(loss_state()) -> boolean().
Cheap predicate for the ecn_socket_dirty flag.
Pure record-field read, no allocation. The flush hot path uses this to
short-circuit out of the rare validation-failure transition without
paying for a tuple return.
-spec on_ack_received(loss_state(), nquic_packet:space(), [{non_neg_integer(), non_neg_integer()}], non_neg_integer(), non_neg_integer(), non_neg_integer()) -> {ok, loss_state(), [nquic_frame:t()], [nquic_frame:t()]}.
Process an ACK frame without ECN counts. Same as on_ack_received/7
with undefined counts.
-spec on_ack_received(loss_state(), nquic_packet:space(), [{non_neg_integer(), non_neg_integer()}], non_neg_integer(), non_neg_integer(), non_neg_integer(), ecn_counts() | undefined) -> {ok, loss_state(), [nquic_frame:t()], [nquic_frame:t()]}.
Process an ACK frame, returning updated state plus acked and lost frames.
MaxAckDelayUs is the peer's advertised max_ack_delay in microseconds.
It is used to compute the persistent congestion window (RFC 9002 Section
7.6.1). Pass 0 if it is not yet known.
An RTT sample is taken when the largest acknowledged packet is newly
acknowledged and at least one newly acknowledged packet is ack-eliciting
(RFC 9002 §5.1). When the ACK newly acknowledges a packet, ECNCounts
go to process_ecn_counts/4 with the send time of the largest newly
acknowledged packet, before loss detection (RFC 9002 Appendix B.7).
-spec on_packet_sent(loss_state(), nquic_packet:space(), nquic_packet_number:t(), [nquic_frame:t()], non_neg_integer(), non_neg_integer()) -> loss_state().
Record a sent packet in the given packet number space.
-spec on_pto(loss_state()) -> loss_state().
Increment the PTO count after a probe timeout fires.
-spec pacer_check(loss_state() | undefined, integer()) -> pass | {block, integer()}.
Decide whether the pacer admits a send at Now (microsecond
monotonic). Returns pass to send immediately or {block, NextUs}
when the caller must hold off until NextUs. Disabled or in-slow-
start pacers always pass.
-spec pacer_config(loss_state()) -> map().
Project the pacer + CC configuration as a map suitable for re-passing
to init/2. Used by code paths that re-initialise loss state on path
/ retry transitions and want to preserve pacing and HyStart++ settings.
-spec pacer_disable(loss_state()) -> loss_state().
Disable the pacer. Used by the caller to opt out at runtime.
-spec pacer_is_enabled(loss_state()) -> boolean().
Whether the pacer is configured on this connection. The pacer may be
present-and-disengaged if cwnd =< ssthresh (still in slow start) or
if the pacing budget has not been built up yet.
-spec pacer_next_send_time(loss_state()) -> undefined | integer().
Earliest microsecond (monotonic, may be negative on Erlang's
monotonic clock) at which the pacer permits the next send.
undefined if the pacer has never engaged on this connection.
-spec path_stats(loss_state()) -> path_stats().
Project a flat path-stats map for nquic_conn:path_stats/1.
Sums per-PN-space ECN counts (peer-reported CE and total) into single
integers so the consumer does not have to know about packet-number
spaces.
-spec process_ecn_counts(loss_state(), nquic_packet:space(), ecn_counts() | undefined, non_neg_integer()) -> loss_state().
Process ECN counts from a received ACK frame (RFC 9002 §7.1).
Compares the CE count of the peer against the last known baseline. When
the CE count increased, the congestion controller gets a congestion
event for a packet sent at SentTime, the send time of the largest
acknowledged packet (RFC 9002 Appendix B.7). A CE increase for a packet
sent before the current recovery period does not reduce the window
again. When the total of the ECN counts decreased (validation failure),
ECN is disabled for the path.
-spec reset_pto_count(loss_state()) -> loss_state().
Reset the PTO count to zero after receiving an ACK.
-spec set_ecn_enabled(loss_state(), boolean()) -> loss_state().
Enable or disable ECN for this connection.
-spec set_max_datagram_size(loss_state(), pos_integer()) -> loss_state().
Set the maximum datagram size used by the congestion controller.
-spec set_pto_count(loss_state(), non_neg_integer()) -> loss_state().
Set the PTO count, the exponent of the PTO backoff.