-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}.