%%% -*- erlang -*- %%% %%% QUIC Congestion Control Behavior and Facade %%% RFC 9002 Section 7 - Congestion Control %%% %%% Copyright (c) 2024-2026 Benoit Chesneau %%% Apache License 2.0 %%% %%% @doc QUIC congestion control behavior and facade. %%% %%% This module defines the behavior for pluggable congestion control %%% algorithms and provides a facade that delegates to the selected %%% implementation. %%% %%% == Available Algorithms == %%% %%% - `newreno' (default): RFC 9002 NewReno implementation %%% - `bbr': BBRv3 (future implementation) %%% %%% == Usage == %%% %%% ``` %%% %% Create with default algorithm (NewReno) %%% State = quic_cc:new(), %%% State = quic_cc:new(#{initial_window => 65536}), %%% %%% %% Create with explicit algorithm %%% State = quic_cc:new(newreno, #{}), %%% State = quic_cc:new(bbr, #{}). %%% ''' -module(quic_cc). %% API - State management -export([ new/0, new/1, new/2 ]). %% API - Congestion control events -export([ on_packet_sent/2, on_packets_acked/2, on_packets_acked/3, on_packets_lost/2, on_congestion_event/2, on_ecn_ce/2, on_persistent_congestion/1, detect_persistent_congestion/3, update_pacing_rate/2, update_mtu/2 ]). %% API - Queries -export([ cwnd/1, ssthresh/1, bytes_in_flight/1, can_send/2, can_send_control/2, available_cwnd/1, in_slow_start/1, in_recovery/1, pacing_allows/2, get_pacing_tokens/2, pacing_delay/2, max_datagram_size/1, min_recovery_duration/1, ecn_ce_counter/1 ]). %% API - Algorithm info -export([ algorithm/1 ]). %%==================================================================== %% Behavior Definition %%==================================================================== %% State management -callback new(Opts :: cc_opts()) -> State :: term(). %% Congestion control events -callback on_packet_sent(State :: term(), Size :: non_neg_integer()) -> State :: term(). -callback on_packets_acked(State :: term(), AckedBytes :: non_neg_integer()) -> State :: term(). -callback on_packets_acked( State :: term(), AckedBytes :: non_neg_integer(), LargestAckedSentTime :: non_neg_integer() ) -> State :: term(). -callback on_packets_lost(State :: term(), LostBytes :: non_neg_integer()) -> State :: term(). -callback on_congestion_event(State :: term(), SentTime :: non_neg_integer()) -> State :: term(). -callback on_ecn_ce(State :: term(), ECNCE :: non_neg_integer()) -> State :: term(). -callback on_persistent_congestion(State :: term()) -> State :: term(). -callback detect_persistent_congestion( LostInfo :: [{non_neg_integer(), non_neg_integer()}], PTO :: non_neg_integer(), State :: term() ) -> boolean(). %% Pacing -callback update_pacing_rate(State :: term(), SmoothedRTT :: non_neg_integer()) -> State :: term(). -callback pacing_allows(State :: term(), Size :: non_neg_integer()) -> boolean(). -callback get_pacing_tokens(State :: term(), Size :: non_neg_integer()) -> {non_neg_integer(), State :: term()}. -callback pacing_delay(State :: term(), Size :: non_neg_integer()) -> non_neg_integer(). %% MTU update -callback update_mtu(State :: term(), NewMTU :: pos_integer()) -> State :: term(). %% Queries -callback cwnd(State :: term()) -> non_neg_integer(). -callback ssthresh(State :: term()) -> non_neg_integer() | infinity. -callback bytes_in_flight(State :: term()) -> non_neg_integer(). -callback can_send(State :: term(), Size :: non_neg_integer()) -> boolean(). -callback can_send_control(State :: term(), Size :: non_neg_integer()) -> boolean(). -callback available_cwnd(State :: term()) -> non_neg_integer(). -callback in_slow_start(State :: term()) -> boolean(). -callback in_recovery(State :: term()) -> boolean(). -callback max_datagram_size(State :: term()) -> pos_integer(). -callback min_recovery_duration(State :: term()) -> non_neg_integer(). -callback ecn_ce_counter(State :: term()) -> non_neg_integer(). %%==================================================================== %% Types %%==================================================================== %% CC algorithm type -type cc_algorithm() :: newreno | bbr | cubic. %% CC options type for implementations -type cc_opts() :: #{ initial_window => pos_integer(), minimum_window => pos_integer(), min_recovery_duration => non_neg_integer(), max_datagram_size => pos_integer(), algorithm => cc_algorithm() }. %% Wrapper state that holds algorithm module and its state -record(cc_wrapper, { algorithm :: module(), state :: term() }). -opaque cc_state() :: #cc_wrapper{}. -export_type([cc_state/0, cc_opts/0, cc_algorithm/0]). %%==================================================================== %% State Management %%==================================================================== %% @doc Create a new congestion control state with default algorithm (NewReno). -spec new() -> cc_state(). new() -> new(#{}). %% @doc Create a new congestion control state with options. %% Uses the algorithm specified in options, or NewReno by default. %% %% Options: %% - algorithm: CC algorithm (`newreno' | `bbr'), default: `newreno' %% - max_datagram_size: Maximum datagram size (default: 1200) %% - initial_window: Override initial congestion window %% - minimum_window: Lower bound for cwnd after congestion events %% - min_recovery_duration: Minimum time in recovery before exit (ms) -spec new(cc_opts()) -> cc_state(). new(Opts) -> Algorithm = maps:get(algorithm, Opts, newreno), new(Algorithm, Opts). %% @doc Create a new congestion control state with explicit algorithm. %% %% Algorithm: `newreno' | `bbr' %% Options: Same as new/1 (algorithm option is ignored) -spec new(cc_algorithm(), cc_opts()) -> cc_state(). new(Algorithm, Opts) -> Module = algorithm_to_module(Algorithm), %% Remove algorithm from opts before passing to implementation ImplOpts = maps:remove(algorithm, Opts), State = Module:new(ImplOpts), #cc_wrapper{ algorithm = Module, state = State }. %%==================================================================== %% Congestion Control Events %%==================================================================== %% @doc Record that a packet was sent. -spec on_packet_sent(cc_state(), non_neg_integer()) -> cc_state(). on_packet_sent(#cc_wrapper{algorithm = Mod, state = State} = W, Size) -> W#cc_wrapper{state = Mod:on_packet_sent(State, Size)}. %% @doc Process acknowledged packets. -spec on_packets_acked(cc_state(), non_neg_integer()) -> cc_state(). on_packets_acked(#cc_wrapper{algorithm = Mod, state = State} = W, AckedBytes) -> W#cc_wrapper{state = Mod:on_packets_acked(State, AckedBytes)}. %% @doc Process acknowledged packets with largest acked sent time. -spec on_packets_acked(cc_state(), non_neg_integer(), non_neg_integer()) -> cc_state(). on_packets_acked( #cc_wrapper{algorithm = Mod, state = State} = W, AckedBytes, LargestAckedSentTime ) -> W#cc_wrapper{state = Mod:on_packets_acked(State, AckedBytes, LargestAckedSentTime)}. %% @doc Process lost packets. -spec on_packets_lost(cc_state(), non_neg_integer()) -> cc_state(). on_packets_lost(#cc_wrapper{algorithm = Mod, state = State} = W, LostBytes) -> W#cc_wrapper{state = Mod:on_packets_lost(State, LostBytes)}. %% @doc Handle a congestion event (packet loss detected). -spec on_congestion_event(cc_state(), non_neg_integer()) -> cc_state(). on_congestion_event(#cc_wrapper{algorithm = Mod, state = State} = W, SentTime) -> W#cc_wrapper{state = Mod:on_congestion_event(State, SentTime)}. %% @doc Handle ECN-CE signal. -spec on_ecn_ce(cc_state(), non_neg_integer()) -> cc_state(). on_ecn_ce(#cc_wrapper{algorithm = Mod, state = State} = W, ECNCE) -> W#cc_wrapper{state = Mod:on_ecn_ce(State, ECNCE)}. %% @doc Handle persistent congestion. -spec on_persistent_congestion(cc_state()) -> cc_state(). on_persistent_congestion(#cc_wrapper{algorithm = Mod, state = State} = W) -> W#cc_wrapper{state = Mod:on_persistent_congestion(State)}. %% @doc Detect persistent congestion from lost packets. -spec detect_persistent_congestion( [{non_neg_integer(), non_neg_integer()}], non_neg_integer(), cc_state() ) -> boolean(). detect_persistent_congestion(LostInfo, PTO, #cc_wrapper{algorithm = Mod, state = State}) -> Mod:detect_persistent_congestion(LostInfo, PTO, State). %% @doc Update pacing rate based on smoothed RTT. -spec update_pacing_rate(cc_state(), non_neg_integer()) -> cc_state(). update_pacing_rate(#cc_wrapper{algorithm = Mod, state = State} = W, SmoothedRTT) -> W#cc_wrapper{state = Mod:update_pacing_rate(State, SmoothedRTT)}. %% @doc Update congestion control state when MTU changes. -spec update_mtu(cc_state(), pos_integer()) -> cc_state(). update_mtu(#cc_wrapper{algorithm = Mod, state = State} = W, NewMTU) -> W#cc_wrapper{state = Mod:update_mtu(State, NewMTU)}. %%==================================================================== %% Queries %%==================================================================== %% @doc Get the current congestion window. -spec cwnd(cc_state()) -> non_neg_integer(). cwnd(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:cwnd(State). %% @doc Get the slow start threshold. -spec ssthresh(cc_state()) -> non_neg_integer() | infinity. ssthresh(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:ssthresh(State). %% @doc Get bytes currently in flight. -spec bytes_in_flight(cc_state()) -> non_neg_integer(). bytes_in_flight(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:bytes_in_flight(State). %% @doc Check if we can send more bytes. -spec can_send(cc_state(), non_neg_integer()) -> boolean(). can_send(#cc_wrapper{algorithm = Mod, state = State}, Size) -> Mod:can_send(State, Size). %% @doc Check if a control message can be sent. -spec can_send_control(cc_state(), non_neg_integer()) -> boolean(). can_send_control(#cc_wrapper{algorithm = Mod, state = State}, Size) -> Mod:can_send_control(State, Size). %% @doc Get the available congestion window. -spec available_cwnd(cc_state()) -> non_neg_integer(). available_cwnd(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:available_cwnd(State). %% @doc Check if in slow start phase. -spec in_slow_start(cc_state()) -> boolean(). in_slow_start(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:in_slow_start(State). %% @doc Check if in recovery phase. -spec in_recovery(cc_state()) -> boolean(). in_recovery(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:in_recovery(State). %% @doc Check if pacing allows sending. -spec pacing_allows(cc_state(), non_neg_integer()) -> boolean(). pacing_allows(#cc_wrapper{algorithm = Mod, state = State}, Size) -> Mod:pacing_allows(State, Size). %% @doc Get pacing tokens for sending. -spec get_pacing_tokens(cc_state(), non_neg_integer()) -> {non_neg_integer(), cc_state()}. get_pacing_tokens(#cc_wrapper{algorithm = Mod, state = State} = W, Size) -> {Allowed, NewState} = Mod:get_pacing_tokens(State, Size), {Allowed, W#cc_wrapper{state = NewState}}. %% @doc Calculate pacing delay. -spec pacing_delay(cc_state(), non_neg_integer()) -> non_neg_integer(). pacing_delay(#cc_wrapper{algorithm = Mod, state = State}, Size) -> Mod:pacing_delay(State, Size). %% @doc Get the current max datagram size. -spec max_datagram_size(cc_state()) -> pos_integer(). max_datagram_size(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:max_datagram_size(State). %% @doc Get minimum recovery duration setting. -spec min_recovery_duration(cc_state()) -> non_neg_integer(). min_recovery_duration(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:min_recovery_duration(State). %% @doc Get the current ECN-CE counter. -spec ecn_ce_counter(cc_state()) -> non_neg_integer(). ecn_ce_counter(#cc_wrapper{algorithm = Mod, state = State}) -> Mod:ecn_ce_counter(State). %% @doc Get the algorithm name for this CC state. -spec algorithm(cc_state()) -> cc_algorithm(). algorithm(#cc_wrapper{algorithm = Mod}) -> module_to_algorithm(Mod). %%==================================================================== %% Internal Functions %%==================================================================== %% Map algorithm atom to implementation module -spec algorithm_to_module(cc_algorithm()) -> module(). algorithm_to_module(newreno) -> quic_cc_newreno; algorithm_to_module(bbr) -> quic_cc_bbr; algorithm_to_module(cubic) -> quic_cc_cubic. %% Map implementation module to algorithm atom -spec module_to_algorithm(module()) -> cc_algorithm(). module_to_algorithm(quic_cc_newreno) -> newreno; module_to_algorithm(quic_cc_bbr) -> bbr; module_to_algorithm(quic_cc_cubic) -> cubic.