quic_ack (quic v2.0.2)

View Source

QUIC ACK frame generation and processing.

Two independent things live here. They share the range form below and nothing else, so read whichever one you came for and ignore the other.

The connection's ACK path (stateless)

Plain functions over a range list, no state record. quic_connection drives these: it keeps its ranges in #pn_space.ack_ranges and never builds an #ack_state{}. Range accumulation, the retained-range cap, ACK frame construction, and the frame classification deciding whether a packet needs acknowledging. quic_loss calls ack_frame_to_ranges/3 from this half when an ACK arrives.

The #ack_state{} accumulator (stateful)

A self-contained receiver: new/0, then record_received/2,3 per packet, then generate_ack/1,2, needs_ack/1 and process_ack/2,3. No production code drives it; only tests do. It is not dead weight: it carries the ACK delay and ECN arithmetic that the stateless half has no equivalent for, and it is the only coverage of that arithmetic.

ACK Ranges

Both halves use the same form: a list of {Start, End} tuples where Start =< End, sorted in descending order by Start. Example: [{100, 105}, {90, 95}, {80, 82}] acknowledges packets 100-105, 90-95, and 80-82.

Summary

Functions

Get the number of ACK-eliciting packets in flight.

Convert an ACK frame to the list of packet numbers it covers.

Convert ACK frame to list of ranges instead of expanded list. Returns a list of {Start, End} tuples where Start is less than or equal to End. Much more efficient than ack_frame_to_pn_list for large ranges. Example: {100, 5, [{2, 3}]} becomes [{95, 100}, {89, 92}]

Get the current ACK ranges.

Add a packet number to a descending, disjoint range list.

Build an encoded ACK frame from internal ranges.

Build an unencoded ACK frame tuple from internal ranges.

Drop the lowest ranges beyond ?MAX_ACK_RANGE_COUNT.

Is any frame in the list ack-eliciting?

Convert internal ACK ranges to encoder format.

Generate an ACK frame for the current state. Returns {ok, AckFrame} or {error, no_packets}.

Generate an ACK frame with a specific timestamp.

Is a decoded frame ack-eliciting?

Get the largest acknowledged packet number.

Get the largest received packet number.

Mark that an ACK was sent.

Merge the head range with the next when they touch or overlap.

Check if an ACK needs to be sent.

Create a new ACK tracking state.

Process a received ACK frame. Returns {NewState, AckedPackets} where AckedPackets is a list of newly acknowledged packet numbers.

Process a received ACK frame with sent packet info. SentPackets is a map of PacketNumber => SentPacketInfo

Split the codec's range list into the shape quic_loss takes.

Record that a packet was received.

Record that a packet was received, optionally marking it as ACK-eliciting.

Record a received packet number in a packet-number space.

Types

ack_state/0

-opaque ack_state()

Functions

ack_eliciting_in_flight(Ack_state)

-spec ack_eliciting_in_flight(ack_state()) -> non_neg_integer().

Get the number of ACK-eliciting packets in flight.

ack_frame_to_pn_list(LargestAcked, FirstRange, AckRanges)

-spec ack_frame_to_pn_list(non_neg_integer(), non_neg_integer(), list()) ->
                              [non_neg_integer()] | {error, ack_range_too_large}.

Convert an ACK frame to the list of packet numbers it covers.

Prefer ack_frame_to_ranges/3 for wide ranges: this expands every packet number into the list.

ack_frame_to_ranges(LargestAcked, FirstRange, AckRanges)

-spec ack_frame_to_ranges(non_neg_integer(), non_neg_integer(), list()) ->
                             [{non_neg_integer(), non_neg_integer()}] | {error, ack_range_too_large}.

Convert ACK frame to list of ranges instead of expanded list. Returns a list of {Start, End} tuples where Start is less than or equal to End. Much more efficient than ack_frame_to_pn_list for large ranges. Example: {100, 5, [{2, 3}]} becomes [{95, 100}, {89, 92}]

ack_ranges(Ack_state)

-spec ack_ranges(ack_state()) -> [{non_neg_integer(), non_neg_integer()}].

Get the current ACK ranges.

add_to_ranges(PN, Rest)

-spec add_to_ranges(non_neg_integer(), [{non_neg_integer(), non_neg_integer()}]) ->
                       [{non_neg_integer(), non_neg_integer()}].

Add a packet number to a descending, disjoint range list.

Ranges touching the new packet number are extended, and extending downward may close a gap, so the head is re-merged.

build_ack_frame(Ranges)

-spec build_ack_frame([{non_neg_integer(), non_neg_integer()}]) -> binary().

Build an encoded ACK frame from internal ranges.

build_ack_frame_tuple(Ranges)

-spec build_ack_frame_tuple([{non_neg_integer(), non_neg_integer()}]) ->
                               {ack,
                                [{non_neg_integer(), non_neg_integer()}],
                                non_neg_integer(),
                                undefined}.

Build an unencoded ACK frame tuple from internal ranges.

The delay is zero: the frame is built at send time, so there is no accumulated delay to report.

cap_ack_ranges(Tail)

-spec cap_ack_ranges([{non_neg_integer(), non_neg_integer()}]) ->
                        [{non_neg_integer(), non_neg_integer()}].

Drop the lowest ranges beyond ?MAX_ACK_RANGE_COUNT.

The list is descending, so the newest packet numbers are kept. Packets below the lowest retained range are retransmitted by the peer and dropped as duplicates.

contains_ack_eliciting_frames(Rest)

-spec contains_ack_eliciting_frames([term()]) -> boolean().

Is any frame in the list ack-eliciting?

The single stream frame produced by every chunked send takes a fast path rather than the list walk.

convert_ack_ranges_for_encode(Rest)

-spec convert_ack_ranges_for_encode([{non_neg_integer(), non_neg_integer()}]) ->
                                       [{non_neg_integer(), non_neg_integer()}].

Convert internal ACK ranges to encoder format.

Internal form is [{Start, End}, ...] descending, where Start =< End. The encoder expects [{LargestAcked, FirstRange}, {Gap, Range}, ...]. The first range is capped at ?MAX_ACK_RANGE so the receiver does not reject the frame.

generate_ack(State)

-spec generate_ack(ack_state()) -> {ok, term()} | {error, no_packets}.

Generate an ACK frame for the current state. Returns {ok, AckFrame} or {error, no_packets}.

generate_ack(Ack_state, Now)

-spec generate_ack(ack_state(), non_neg_integer()) -> {ok, term()} | {error, no_packets}.

Generate an ACK frame with a specific timestamp.

is_ack_eliciting_frame(_)

-spec is_ack_eliciting_frame(term()) -> boolean().

Is a decoded frame ack-eliciting?

Per RFC 9002, ACK, PADDING and CONNECTION_CLOSE are not.

largest_acked(Ack_state)

-spec largest_acked(ack_state()) -> non_neg_integer() | undefined.

Get the largest acknowledged packet number.

largest_received(Ack_state)

-spec largest_received(ack_state()) -> non_neg_integer() | undefined.

Get the largest received packet number.

mark_ack_sent(State)

-spec mark_ack_sent(ack_state()) -> ack_state().

Mark that an ACK was sent.

merge_ranges(Rest)

-spec merge_ranges([{non_neg_integer(), non_neg_integer()}]) -> [{non_neg_integer(), non_neg_integer()}].

Merge the head range with the next when they touch or overlap.

needs_ack(Ack_state)

-spec needs_ack(ack_state()) -> boolean().

Check if an ACK needs to be sent.

new()

-spec new() -> ack_state().

Create a new ACK tracking state.

process_ack(State, AckFrame)

-spec process_ack(ack_state(), term()) -> {ack_state(), [non_neg_integer()]}.

Process a received ACK frame. Returns {NewState, AckedPackets} where AckedPackets is a list of newly acknowledged packet numbers.

process_ack(State, _, SentPackets)

-spec process_ack(ack_state(), term(), map()) ->
                     {ack_state(), [non_neg_integer()]} | {error, ack_range_too_large}.

Process a received ACK frame with sent packet info. SentPackets is a map of PacketNumber => SentPacketInfo

ranges_to_ack_format(RestRanges)

-spec ranges_to_ack_format([{non_neg_integer(), non_neg_integer()}]) ->
                              {non_neg_integer(), [{non_neg_integer(), non_neg_integer()}]}.

Split the codec's range list into the shape quic_loss takes.

Drops the largest acked, which the caller already holds.

record_received(State, PacketNumber)

-spec record_received(ack_state(), non_neg_integer()) -> ack_state().

Record that a packet was received.

record_received(Ack_state, PacketNumber, IsAckEliciting)

-spec record_received(ack_state(), non_neg_integer(), boolean()) -> ack_state().

Record that a packet was received, optionally marking it as ACK-eliciting.

update_pn_space_recv(PN, Pn_space, Now)

-spec update_pn_space_recv(non_neg_integer(),
                           #pn_space{next_pn :: non_neg_integer(),
                                     largest_recv :: non_neg_integer() | undefined,
                                     recv_time :: non_neg_integer() | undefined,
                                     ack_ranges :: [{non_neg_integer(), non_neg_integer()}],
                                     ack_eliciting_in_flight :: non_neg_integer()},
                           non_neg_integer()) ->
                              #pn_space{next_pn :: non_neg_integer(),
                                        largest_recv :: non_neg_integer() | undefined,
                                        recv_time :: non_neg_integer() | undefined,
                                        ack_ranges :: [{non_neg_integer(), non_neg_integer()}],
                                        ack_eliciting_in_flight :: non_neg_integer()}.

Record a received packet number in a packet-number space.

A packet continuing the receive sequence extends the head range in place, so the range count cannot grow and the cap scan is skipped.