Skip to main content

src/nquic_protocol_timer.erl

-module(nquic_protocol_timer).
-moduledoc """
Timer-action orchestration for the QUIC protocol state.

Pure functions over `#conn_state{}` that translate connection state
into the `set_timer` / `cancel_timer` actions the owner must schedule:
the ACK-delay timer, the idle timer (RFC 9000 Section 10.1), the PTO
timer (RFC 9002 Section 6.2), the path-validation timer (RFC 9000
Section 8.2), and the PMTUD probe timer (RFC 8899).

`compute_timer_actions/1` is the aggregate entry point used after
`nquic_protocol:handle_packet/3` and `nquic_protocol:flush/1`. The
idle and PTO values are cached in `#conn_state{}` so unchanged timers
do not re-emit actions. External side effects are limited to
delegating into `nquic_loss`, `nquic_pmtud`, and `nquic_path`; the
idle bound is resolved via `nquic_protocol:get_idle_timeout/2`.
""".

-include("nquic_conn.hrl").
-include("nquic_transport.hrl").
-export([
    compute_path_validation_timeout/1,
    compute_pto_timer_actions/1,
    compute_timer_actions/1
]).

-spec compute_ack_delay_timer_acc(nquic_protocol:state(), [nquic_protocol:timeout_action()]) ->
    [nquic_protocol:timeout_action()].
compute_ack_delay_timer_acc(#conn_state{pending_ack_count = 0}, Acc) ->
    Acc;
compute_ack_delay_timer_acc(_State, Acc) ->
    [{set_timer, ack_delay, 1} | Acc].

-spec compute_idle_timer_cached(nquic_protocol:state(), [nquic_protocol:timeout_action()]) ->
    {[nquic_protocol:timeout_action()], nquic_protocol:state()}.
compute_idle_timer_cached(
    #conn_state{local_params = LP, remote_params = RP, last_idle_ms = Cached} = State, Acc
) ->
    Local = LP#transport_params.max_idle_timeout,
    Remote =
        case RP of
            undefined -> 0;
            #transport_params{max_idle_timeout = R} -> R
        end,
    case nquic_protocol:get_idle_timeout(Local, Remote) of
        infinity when Cached =:= infinity ->
            {Acc, State};
        infinity ->
            {Acc, State#conn_state{last_idle_ms = infinity}};
        Timeout when Timeout =:= Cached ->
            {Acc, State};
        Timeout ->
            {[{set_timer, idle, Timeout} | Acc], State#conn_state{last_idle_ms = Timeout}}
    end.

-spec compute_path_timer_acc(nquic_protocol:state(), [nquic_protocol:timeout_action()]) ->
    [nquic_protocol:timeout_action()].
compute_path_timer_acc(#conn_state{path = #conn_path_mgmt{path_state = undefined}}, Acc) ->
    Acc;
compute_path_timer_acc(#conn_state{path = #conn_path_mgmt{path_state = PS}} = State, Acc) ->
    case nquic_path:is_validating(PS) of
        true ->
            [{set_timer, path_validation, compute_path_validation_timeout(State)} | Acc];
        false ->
            Acc
    end.

-spec compute_path_validation_timeout(nquic_protocol:state()) -> non_neg_integer().
compute_path_validation_timeout(#conn_state{loss_state = LS, remote_params = RP}) ->
    MadUs =
        case RP of
            undefined -> 0;
            #transport_params{max_ack_delay = M} -> M * 1000
        end,
    PtoUs = nquic_loss:get_pto_timeout(LS, MadUs),
    max(1, ((3 * PtoUs) + 999) div 1000).

-spec compute_pmtud_timer_acc(nquic_protocol:state(), [nquic_protocol:timeout_action()]) ->
    [nquic_protocol:timeout_action()].
compute_pmtud_timer_acc(#conn_state{pmtud = undefined}, Acc) ->
    Acc;
compute_pmtud_timer_acc(#conn_state{pmtud = PS}, Acc) ->
    case nquic_pmtud:get_timer_ms(PS) of
        infinity -> Acc;
        Ms -> [{set_timer, pmtud, Ms} | Acc]
    end.

-spec compute_pto_ms(nquic_loss:loss_state(), #transport_params{} | undefined) ->
    pos_integer().
compute_pto_ms(LS, RP) ->
    MadUs =
        case RP of
            undefined -> 0;
            #transport_params{max_ack_delay = M} -> M * 1000
        end,
    PtoUs = nquic_loss:get_pto_timeout(LS, MadUs),
    max(1, (PtoUs + 999) div 1000).

-spec compute_pto_timer_actions(nquic_protocol:state()) -> [nquic_protocol:timeout_action()].
compute_pto_timer_actions(#conn_state{loss_state = LS, remote_params = RP}) ->
    case nquic_loss:has_ack_eliciting_in_flight(LS) of
        true ->
            PtoMs = compute_pto_ms(LS, RP),
            [{set_timer, pto, PtoMs}];
        false ->
            [{cancel_timer, pto}]
    end.

-spec compute_pto_timer_cached(nquic_protocol:state(), [nquic_protocol:timeout_action()]) ->
    {[nquic_protocol:timeout_action()], nquic_protocol:state()}.
compute_pto_timer_cached(
    #conn_state{loss_state = LS, remote_params = RP, last_pto_ms = Cached} = State, Acc
) ->
    case nquic_loss:has_ack_eliciting_in_flight(LS) of
        true ->
            PtoMs = compute_pto_ms(LS, RP),
            case Cached of
                PtoMs -> {Acc, State};
                _ -> {[{set_timer, pto, PtoMs} | Acc], State#conn_state{last_pto_ms = PtoMs}}
            end;
        false ->
            case Cached of
                cancel -> {Acc, State};
                _ -> {[{cancel_timer, pto} | Acc], State#conn_state{last_pto_ms = cancel}}
            end
    end.

-doc """
Compute timer actions based on current state.
Returns idle and PTO timer actions the caller should schedule.
Call after `nquic_protocol:handle_packet/3` or `nquic_protocol:flush/1`
to keep timers up to date. Caches idle and PTO values to skip
unchanged timer actions. Uses accumulator to avoid `++` overhead.
""".
-spec compute_timer_actions(nquic_protocol:state()) ->
    {[nquic_protocol:timeout_action()], nquic_protocol:state()}.
compute_timer_actions(State) ->
    Acc0 = compute_ack_delay_timer_acc(State, []),
    Acc1 = compute_path_timer_acc(State, Acc0),
    Acc2 = compute_pmtud_timer_acc(State, Acc1),
    {Acc3, State1} = compute_pto_timer_cached(State, Acc2),
    {Acc4, State2} = compute_idle_timer_cached(State1, Acc3),
    {Acc4, State2}.