%%-------------------------------------------------------------------- %% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -module(quicer_lib). -include("quicer_types.hrl"). -export_type([ cb_ret/0, cb_state/0 ]). -type cb_ret() :: cb_ret_noreply() | cb_ret_reply(). -type cb_state() :: term(). -type cb_ret_reply() :: {reply, Reply :: term(), cb_state()} | {reply, Reply :: term(), cb_state(), action()} | cb_ret_stop_reply(). %% ok and update cb_state -type cb_ret_noreply() :: {ok, cb_state()} %% error handling per callback | {error, Reason :: term(), cb_state()} | {action(), cb_state()} | cb_ret_stop_noreply(). -type cb_ret_stop_noreply() :: {stop, Reason :: term(), cb_state()}. -type cb_ret_stop_reply() :: {stop, Reason :: term(), Reply :: term(), cb_state()}. -type action() :: hibernate | timeout() | {continue, Continue :: term()}. -export([ default_cb_ret/2, handle_dgram_send_states/1, handle_dgram_send_states/3, probe/2 ]). -spec default_cb_ret(cb_ret(), State :: term()) -> {reply, NewState :: term()} | {reply, NewState :: term(), action()} | {noreply, NewState :: term()} | {noreply, NewState :: term(), action()} | {stop, Reason :: term(), Reply :: term(), NewState :: term()} | {stop, Reason :: term(), NewState :: term()}. default_cb_ret({ok, NewCBState}, State) -> %% ok {noreply, State#{callback_state := NewCBState}}; default_cb_ret({hibernate, NewCBState}, State) -> %% hibernate {noreply, State#{callback_state := NewCBState}, hibernate}; default_cb_ret({Timeout, NewCBState}, State) when is_integer(Timeout) -> %% timeout {noreply, State#{callback_state := NewCBState}, Timeout}; default_cb_ret({{continue, _} = Continue, NewCBState}, State) -> %% continue {noreply, State#{callback_state := NewCBState}, Continue}; default_cb_ret({stop, Reason, NewCBState}, State) -> %% stop {stop, Reason, State#{callback_state := NewCBState}}; default_cb_ret({reply, Reply, NewCBState, Action}, State) -> {reply, Reply, State#{callback_state := NewCBState}, Action}; default_cb_ret({reply, Reply, NewCBState}, State) -> {reply, Reply, State#{callback_state := NewCBState}}. -spec probe(connection_handle(), timeout()) -> probe_res(). probe(Conn, Timeout) -> case quicer_nif:send_dgram(Conn, <<>>, _IsSync = 1) of {ok, _Len} -> handle_dgram_send_states(Conn, probe_dgram_send_cb(), Timeout); {error, E} -> {error, dgram_send_error, E}; E -> E end. -spec handle_dgram_send_states(connection_handle()) -> ok | {error, dgram_send_canceled | dgram_send_unknown | dgram_send_lost_discarded}. handle_dgram_send_states(Conn) -> handle_dgram_send_states(init, Conn, default_dgram_suspect_lost_cb(), 5000). -type lost_suspect_callback() :: {fun((connection_handle(), term(), term()) -> term()), term()} | {atom(), term()}. -spec handle_dgram_send_states(connection_handle(), lost_suspect_callback(), timeout()) -> any(). handle_dgram_send_states(Conn, {_CBFun, _CBState} = CB, Timeout) -> handle_dgram_send_states(init, Conn, CB, Timeout). handle_dgram_send_states(init, Conn, {Fun, CallbackState}, Timeout) -> receive {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_SENT}} -> NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState), handle_dgram_send_states(sent, Conn, {Fun, NewCBState}, Timeout); {quic, dgram_send_state, Conn, #{state := Final}} -> Fun(Conn, Final, CallbackState) after 5000 -> %% @TODO proper test caught this, may fire a bug report to msquic Fun(Conn, timeout, CallbackState) end; handle_dgram_send_states(sent, Conn, {Fun, CallbackState}, Timeout) -> receive {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_LOST_SUSPECT}} -> %% Lost suspected, call the callback for the return hits. %% however, we still need to wait for the final state. NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState), receive {quic, dgram_send_state, Conn, #{state := EState}} -> Fun(Conn, EState, NewCBState) after Timeout -> %% @TODO proper test caught this, may fire a bug report to msquic Fun(Conn, timeout, CallbackState) end; {quic, dgram_send_state, Conn, #{state := Final}} -> Fun(Conn, Final, CallbackState) after Timeout -> %% @TODO proper test caught this, may fire a bug report to msquic Fun(Conn, timeout, CallbackState) end. %% Default Callback for Datagram Send lost suspected default_dgram_suspect_lost_cb() -> Fun = fun(_Conn, _, _CallbackState) -> %% just return ok, even it is lost, we don't care. ok end, {Fun, undefined}. probe_dgram_send_cb() -> Fun = fun (_Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState) -> CallbackState#probe_state{sent_at = ts_ms()}; (_Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState) -> CallbackState#probe_state{suspect_lost_at = ts_ms()}; (_Conn, State, CallbackState) -> CallbackState#probe_state{ final_at = ts_ms(), final = State } end, {Fun, #probe_state{}}. ts_ms() -> erlang:monotonic_time(millisecond).