Skip to content

Commit

Permalink
feat: conn probe with unreliable data gram
Browse files Browse the repository at this point in the history
  • Loading branch information
qzhuyan committed Dec 11, 2024
1 parent 63f93fa commit ac85fa0
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 32 deletions.
7 changes: 7 additions & 0 deletions include/quicer.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,11 @@
-define(QUIC_CONGESTION_CONTROL_ALGORITHM_CUBIC, 0).
-define(QUIC_CONGESTION_CONTROL_ALGORITHM_BBR, 1).

-record(probe_state, {
final :: term() | undefined,
sent_at :: integer() | undefined,
suspect_lost_at :: integer() | undefined,
final_at :: integer() | undefined
}).

-endif. %% QUICER_HRL
6 changes: 6 additions & 0 deletions include/quicer_types.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -506,5 +506,11 @@
dgram_max_len := uint64()
}.

-type probe_state() :: #probe_state{}.
-type probe_res() ::
#probe_state{}
| {error, dgram_send_error, atom()}
| {error, atom()}.

%% QUICER_TYPES_HRL
-endif.
24 changes: 18 additions & 6 deletions src/quicer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
close_connection/4,
async_close_connection/1,
async_close_connection/3,
probe/2,
accept_stream/2,
accept_stream/3,
async_accept_stream/2,
Expand Down Expand Up @@ -177,7 +178,10 @@
quicer_addr/0,

%% Registraion Profiles
registration_profile/0
registration_profile/0,

%% probes
probe_res/0
]).

-type connection_opts() :: proplists:proplist() | conn_opts().
Expand Down Expand Up @@ -857,10 +861,13 @@ do_recv(Stream, Count, Buff) ->
async_send_dgram(Conn, Data) ->
quicer_nif:send_dgram(Conn, Data, _IsSyncRel = 1).

%% @doc Sending Unreliable Datagram, returns the end state.
%% @doc Sending Unreliable Datagram
%% return error only if sending could not be scheduled such as
%% not_enough_mem, connection is already closed or wrong args.
%% otherwise, it is fire and forget.
%%
%% %% ref: [https://datatracker.ietf.org/doc/html/rfc9221]
%% @see send/2, async_send_dgram
%% @see send/2, async_send_dgram/2
-spec send_dgram(connection_handle(), binary()) ->
{ok, BytesSent :: non_neg_integer()}
| {error, badarg | not_enough_mem | closed}
Expand All @@ -874,12 +881,17 @@ send_dgram(Conn, Data) ->
{error, E} ->
{error, dgram_send_error, E}
end;
{error, _, _} = E ->
E;
{error, E} ->
{error, dgram_send_error, E}
{error, E};
E ->
E
end.

%% @doc Probe conn state with 0 len dgram.
-spec probe(connection_handle(), timeout()) -> probe_res().
probe(Conn, Timeout) ->
quicer_lib:probe(Conn, Timeout).

%% @doc Shutdown stream gracefully, with infinity timeout
%%
%% @see shutdown_stream/1
Expand Down
93 changes: 70 additions & 23 deletions src/quicer_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
cb_ret/0,
cb_state/0
]).

-type cb_ret() :: cb_ret_noreply() | cb_ret_reply().
-type cb_state() :: term().

Expand All @@ -43,7 +44,9 @@

-export([
default_cb_ret/2,
handle_dgram_send_states/1
handle_dgram_send_states/1,
handle_dgram_send_states/3,
probe/2
]).

-spec default_cb_ret(cb_ret(), State :: term()) ->
Expand Down Expand Up @@ -73,42 +76,86 @@ default_cb_ret({reply, Reply, NewCBState, Action}, State) ->
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).
handle_dgram_send_states(init, 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}} ->
handle_dgram_send_states(sent, Conn);
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
%% @TODO unsure if it will hit here
ok;
{quic, dgram_send_state, Conn, #{state := E}} ->
{error, E}
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 ->
Fun(Conn, timeout, CallbackState)
end;
handle_dgram_send_states(sent, Conn) ->
handle_dgram_send_states(sent, Conn, {Fun, CallbackState}, Timeout) ->
receive
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
%% Happy Track
ok;
%% {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
%% %% Happy Track
%% Fun(Conn, ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED, CallbackState);
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_LOST_SUSPECT}} ->
%% Lost suspected
%% 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 := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED_SPURIOUS
}} ->
%% Lost recovered
ok;
{quic, dgram_send_state, Conn, #{state := EState}} ->
%% Unrecoverable Errors.
{error, EState}
Fun(Conn, EState, NewCBState)
after Timeout ->
Fun(Conn, timeout, CallbackState)
end;
{quic, dgram_send_state, Conn, #{state := EState}} ->
{quic, dgram_send_state, Conn, #{state := Final}} ->
%% Unrecoverable Errors.
{error, EState}
Fun(Conn, Final, CallbackState)
after Timeout ->
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).
39 changes: 37 additions & 2 deletions test/prop_stateful_client_conn.erl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ prop_client_state_test() ->
%%%%%%%%%%%%%
%% @doc Initial model value at system start. Should be deterministic.
initial_state() ->
net_kernel:start([?MODULE, shortnames]),
{ok, H} = quicer:connect("localhost", 14568, default_conn_opts(), 10000),
#{
state => connected,
Expand All @@ -82,6 +83,8 @@ command(#{handle := Handle}) ->
]}},
{100, {call, quicer, peername, [Handle]}},
{50, {call, quicer, peercert, [Handle]}},
{50, {call, quicer, probe, [Handle, 5000]}},
{50, {call, quicer, send_dgram, [Handle, binary()]}},
{10, {call, quicer, negotiated_protocol, [Handle]}},
{10, {call, quicer, get_connections, []}},
{10, {call, quicer, get_conn_owner, [Handle]}},
Expand Down Expand Up @@ -198,6 +201,36 @@ postcondition(
{error, not_owner}
) ->
Owner =/= self();
postcondition(
#{state := ConnState},
{call, quicer, probe, [_, _]},
{error, dgram_send_error, _}
) ->
ConnState =/= connected;
postcondition(
#{state := _ConnState},
{call, quicer, probe, [_, _]},
#probe_state{final = FinalState, final_at = FinalTs}
) ->
FinalState =/= undefined andalso FinalTs =/= undefined;
postcondition(
#{state := _ConnState},
{call, quicer, send_dgram, [_, _]},
{ok, _}
) ->
true;
postcondition(
#{state := ConnState},
{call, quicer, send_dgram, [_, _]},
{error, _, _}
) ->
ConnState =/= connected;
postcondition(
#{state := ConnState},
{call, quicer, send_dgram, [_, _]},
{error, _}
) ->
ConnState =/= connected;
postcondition(
#{owner := _, state := connected},
{call, quicer, controlling_process, [_, NewOwner]},
Expand Down Expand Up @@ -275,7 +308,8 @@ default_listen_opts() ->
{handshake_idle_timeout_ms, 10000},
% QUIC_SERVER_RESUME_AND_ZERORTT
{server_resumption_level, 2},
{peer_bidi_stream_count, 10}
{peer_bidi_stream_count, 10},
{datagram_receive_enabled, 1}
].

default_conn_opts() ->
Expand All @@ -286,7 +320,8 @@ default_conn_opts() ->
{idle_timeout_ms, 0},
{cacertfile, "./msquic/submodules/openssl/test/certs/rootCA.pem"},
{certfile, "./msquic/submodules/openssl/test/certs/servercert.pem"},
{keyfile, "./msquic/submodules/openssl/test/certs/serverkey.pem"}
{keyfile, "./msquic/submodules/openssl/test/certs/serverkey.pem"},
{datagram_receive_enabled, 1}
].

%% Test helpers
Expand Down
36 changes: 36 additions & 0 deletions test/prop_stateful_server_conn.erl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ command(#{handle := Handle}) ->
{call, quicer, async_accept_stream, [Handle, ?LET(Opts, quicer_acceptor_opts(), Opts)]}},
{100, {call, quicer, peername, [Handle]}},
{50, {call, quicer, peercert, [Handle]}},
{50, {call, quicer, probe, [Handle, 5000]}},
{50, {call, quicer, send_dgram, [Handle, binary()]}},
{10, {call, quicer, negotiated_protocol, [Handle]}},
{10, {call, quicer, get_connections, []}},
{10, {call, quicer, get_conn_owner, [Handle]}},
Expand Down Expand Up @@ -295,6 +297,36 @@ postcondition(#{state := closed}, {call, _Mod, _Fun, _Args}, {error, closed}) ->
postcondition(#{state := accepted}, {call, _Mod, _Fun, _Args}, {error, closed}) ->
%% handshake didnt take place on time
true;
postcondition(
#{state := ConnState},
{call, quicer, probe, [_, _]},
{error, dgram_send_error, _}
) ->
ConnState =/= connected;
postcondition(
#{state := _ConnState},
{call, quicer, probe, [_, _]},
#probe_state{final = FinalState, final_at = FinalTs}
) ->
FinalState =/= undefined andalso FinalTs =/= undefined;
postcondition(
#{state := _ConnState},
{call, quicer, send_dgram, [_, _]},
{ok, _}
) ->
true;
postcondition(
#{state := ConnState},
{call, quicer, send_dgram, [_, _]},
{error, _, _}
) ->
ConnState =/= connected;
postcondition(
#{state := ConnState},
{call, quicer, send_dgram, [_, _]},
{error, _}
) ->
ConnState =/= connected;
postcondition(_State, {call, _Mod, _Fun, _Args} = _Call, _Res) ->
false.

Expand All @@ -321,6 +353,10 @@ do_next_state(
#{state := _} = State, ok, {call, quicer, controlling_process, [_, Owner]}
) ->
State#{owner := Owner};
do_next_state(
#{state := _} = State, {error, closed}, {call, _M, _F, _A}
) ->
State#{state := closed};
do_next_state(State, _Res, {call, _Mod, _Fun, _Args}) ->
State.

Expand Down
3 changes: 2 additions & 1 deletion test/quicer_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,8 @@ tc_dgram_client_send_fail(_) ->
Opts = default_conn_opts() ++ [{datagram_receive_enabled, 1}],
{ok, Conn} = quicer:async_connect("localhost", 65535, Opts),
?assertEqual(
{error, dgram_send_error, dgram_send_canceled},
%% fire and forget
{ok, 4},
quicer:send_dgram(Conn, <<"ping">>)
),
ok.
Expand Down
10 changes: 10 additions & 0 deletions test/quicer_connection_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,16 @@ tc_closed_conn_reg(_Config) ->
Opts = default_conn_opts() ++ [{quic_registration, ThisReg}],
?assertEqual({error, quic_registration}, quicer:connect("localhost", 443, Opts, 5000)).

tc_conn_probe(_) ->
Opts = default_conn_opts() ++ [{datagram_receive_enabled, 1}],
{ok, Conn} = quicer:async_connect("localhost", 65535, Opts),
?assertMatch(
#probe_state{final_at = TS, final = ?QUIC_DATAGRAM_SEND_CANCELED} when
TS =/= undefined,
quicer:probe(Conn, 5000)
),
ok.

%%%
%%% Helpers
%%%
Expand Down

0 comments on commit ac85fa0

Please sign in to comment.