Skip to content

Commit ac85fa0

Browse files
committed
feat: conn probe with unreliable data gram
1 parent 63f93fa commit ac85fa0

8 files changed

+186
-32
lines changed

include/quicer.hrl

+7
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,11 @@
149149
-define(QUIC_CONGESTION_CONTROL_ALGORITHM_CUBIC, 0).
150150
-define(QUIC_CONGESTION_CONTROL_ALGORITHM_BBR, 1).
151151

152+
-record(probe_state, {
153+
final :: term() | undefined,
154+
sent_at :: integer() | undefined,
155+
suspect_lost_at :: integer() | undefined,
156+
final_at :: integer() | undefined
157+
}).
158+
152159
-endif. %% QUICER_HRL

include/quicer_types.hrl

+6
Original file line numberDiff line numberDiff line change
@@ -506,5 +506,11 @@
506506
dgram_max_len := uint64()
507507
}.
508508

509+
-type probe_state() :: #probe_state{}.
510+
-type probe_res() ::
511+
#probe_state{}
512+
| {error, dgram_send_error, atom()}
513+
| {error, atom()}.
514+
509515
%% QUICER_TYPES_HRL
510516
-endif.

src/quicer.erl

+18-6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
close_connection/4,
6262
async_close_connection/1,
6363
async_close_connection/3,
64+
probe/2,
6465
accept_stream/2,
6566
accept_stream/3,
6667
async_accept_stream/2,
@@ -177,7 +178,10 @@
177178
quicer_addr/0,
178179

179180
%% Registraion Profiles
180-
registration_profile/0
181+
registration_profile/0,
182+
183+
%% probes
184+
probe_res/0
181185
]).
182186

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

860-
%% @doc Sending Unreliable Datagram, returns the end state.
864+
%% @doc Sending Unreliable Datagram
865+
%% return error only if sending could not be scheduled such as
866+
%% not_enough_mem, connection is already closed or wrong args.
867+
%% otherwise, it is fire and forget.
861868
%%
862869
%% %% ref: [https://datatracker.ietf.org/doc/html/rfc9221]
863-
%% @see send/2, async_send_dgram
870+
%% @see send/2, async_send_dgram/2
864871
-spec send_dgram(connection_handle(), binary()) ->
865872
{ok, BytesSent :: non_neg_integer()}
866873
| {error, badarg | not_enough_mem | closed}
@@ -874,12 +881,17 @@ send_dgram(Conn, Data) ->
874881
{error, E} ->
875882
{error, dgram_send_error, E}
876883
end;
877-
{error, _, _} = E ->
878-
E;
879884
{error, E} ->
880-
{error, dgram_send_error, E}
885+
{error, E};
886+
E ->
887+
E
881888
end.
882889

890+
%% @doc Probe conn state with 0 len dgram.
891+
-spec probe(connection_handle(), timeout()) -> probe_res().
892+
probe(Conn, Timeout) ->
893+
quicer_lib:probe(Conn, Timeout).
894+
883895
%% @doc Shutdown stream gracefully, with infinity timeout
884896
%%
885897
%% @see shutdown_stream/1

src/quicer_lib.erl

+70-23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
cb_ret/0,
2121
cb_state/0
2222
]).
23+
2324
-type cb_ret() :: cb_ret_noreply() | cb_ret_reply().
2425
-type cb_state() :: term().
2526

@@ -43,7 +44,9 @@
4344

4445
-export([
4546
default_cb_ret/2,
46-
handle_dgram_send_states/1
47+
handle_dgram_send_states/1,
48+
handle_dgram_send_states/3,
49+
probe/2
4750
]).
4851

4952
-spec default_cb_ret(cb_ret(), State :: term()) ->
@@ -73,42 +76,86 @@ default_cb_ret({reply, Reply, NewCBState, Action}, State) ->
7376
default_cb_ret({reply, Reply, NewCBState}, State) ->
7477
{reply, Reply, State#{callback_state := NewCBState}}.
7578

79+
-spec probe(connection_handle(), timeout()) -> probe_res().
80+
probe(Conn, Timeout) ->
81+
case quicer_nif:send_dgram(Conn, <<>>, _IsSync = 1) of
82+
{ok, _Len} ->
83+
handle_dgram_send_states(Conn, probe_dgram_send_cb(), Timeout);
84+
{error, E} ->
85+
{error, dgram_send_error, E};
86+
E ->
87+
E
88+
end.
89+
7690
-spec handle_dgram_send_states(connection_handle()) ->
7791
ok
7892
| {error,
7993
dgram_send_canceled
8094
| dgram_send_unknown
8195
| dgram_send_lost_discarded}.
8296
handle_dgram_send_states(Conn) ->
83-
handle_dgram_send_states(init, Conn).
84-
handle_dgram_send_states(init, Conn) ->
97+
handle_dgram_send_states(init, Conn, default_dgram_suspect_lost_cb(), 5000).
98+
99+
-type lost_suspect_callback() ::
100+
{fun((connection_handle(), term(), term()) -> term()), term()}
101+
| {atom(), term()}.
102+
-spec handle_dgram_send_states(connection_handle(), lost_suspect_callback(), timeout()) -> any().
103+
handle_dgram_send_states(Conn, {_CBFun, _CBState} = CB, Timeout) ->
104+
handle_dgram_send_states(init, Conn, CB, Timeout).
105+
106+
handle_dgram_send_states(init, Conn, {Fun, CallbackState}, Timeout) ->
85107
receive
86108
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_SENT}} ->
87-
handle_dgram_send_states(sent, Conn);
88-
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
89-
%% @TODO unsure if it will hit here
90-
ok;
91-
{quic, dgram_send_state, Conn, #{state := E}} ->
92-
{error, E}
109+
NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState),
110+
handle_dgram_send_states(sent, Conn, {Fun, NewCBState}, Timeout);
111+
{quic, dgram_send_state, Conn, #{state := Final}} ->
112+
Fun(Conn, Final, CallbackState)
113+
after 5000 ->
114+
Fun(Conn, timeout, CallbackState)
93115
end;
94-
handle_dgram_send_states(sent, Conn) ->
116+
handle_dgram_send_states(sent, Conn, {Fun, CallbackState}, Timeout) ->
95117
receive
96-
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
97-
%% Happy Track
98-
ok;
118+
%% {quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED}} ->
119+
%% %% Happy Track
120+
%% Fun(Conn, ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED, CallbackState);
99121
{quic, dgram_send_state, Conn, #{state := ?QUIC_DATAGRAM_SEND_LOST_SUSPECT}} ->
100-
%% Lost suspected
122+
%% Lost suspected, call the callback for the return hits.
123+
%% however, we still need to wait for the final state.
124+
NewCBState = Fun(Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState),
101125
receive
102-
{quic, dgram_send_state, Conn, #{
103-
state := ?QUIC_DATAGRAM_SEND_ACKNOWLEDGED_SPURIOUS
104-
}} ->
105-
%% Lost recovered
106-
ok;
107126
{quic, dgram_send_state, Conn, #{state := EState}} ->
108-
%% Unrecoverable Errors.
109-
{error, EState}
127+
Fun(Conn, EState, NewCBState)
128+
after Timeout ->
129+
Fun(Conn, timeout, CallbackState)
110130
end;
111-
{quic, dgram_send_state, Conn, #{state := EState}} ->
131+
{quic, dgram_send_state, Conn, #{state := Final}} ->
112132
%% Unrecoverable Errors.
113-
{error, EState}
133+
Fun(Conn, Final, CallbackState)
134+
after Timeout ->
135+
Fun(Conn, timeout, CallbackState)
114136
end.
137+
138+
%% Default Callback for Datagram Send lost suspected
139+
default_dgram_suspect_lost_cb() ->
140+
Fun = fun(_Conn, _, _CallbackState) ->
141+
%% just return ok, even it is lost, we don't care.
142+
ok
143+
end,
144+
{Fun, undefined}.
145+
146+
probe_dgram_send_cb() ->
147+
Fun = fun
148+
(_Conn, ?QUIC_DATAGRAM_SEND_SENT, CallbackState) ->
149+
CallbackState#probe_state{sent_at = ts_ms()};
150+
(_Conn, ?QUIC_DATAGRAM_SEND_LOST_SUSPECT, CallbackState) ->
151+
CallbackState#probe_state{suspect_lost_at = ts_ms()};
152+
(_Conn, State, CallbackState) ->
153+
CallbackState#probe_state{
154+
final_at = ts_ms(),
155+
final = State
156+
}
157+
end,
158+
{Fun, #probe_state{}}.
159+
160+
ts_ms() ->
161+
erlang:monotonic_time(millisecond).

test/prop_stateful_client_conn.erl

+37-2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ prop_client_state_test() ->
5959
%%%%%%%%%%%%%
6060
%% @doc Initial model value at system start. Should be deterministic.
6161
initial_state() ->
62+
net_kernel:start([?MODULE, shortnames]),
6263
{ok, H} = quicer:connect("localhost", 14568, default_conn_opts(), 10000),
6364
#{
6465
state => connected,
@@ -82,6 +83,8 @@ command(#{handle := Handle}) ->
8283
]}},
8384
{100, {call, quicer, peername, [Handle]}},
8485
{50, {call, quicer, peercert, [Handle]}},
86+
{50, {call, quicer, probe, [Handle, 5000]}},
87+
{50, {call, quicer, send_dgram, [Handle, binary()]}},
8588
{10, {call, quicer, negotiated_protocol, [Handle]}},
8689
{10, {call, quicer, get_connections, []}},
8790
{10, {call, quicer, get_conn_owner, [Handle]}},
@@ -198,6 +201,36 @@ postcondition(
198201
{error, not_owner}
199202
) ->
200203
Owner =/= self();
204+
postcondition(
205+
#{state := ConnState},
206+
{call, quicer, probe, [_, _]},
207+
{error, dgram_send_error, _}
208+
) ->
209+
ConnState =/= connected;
210+
postcondition(
211+
#{state := _ConnState},
212+
{call, quicer, probe, [_, _]},
213+
#probe_state{final = FinalState, final_at = FinalTs}
214+
) ->
215+
FinalState =/= undefined andalso FinalTs =/= undefined;
216+
postcondition(
217+
#{state := _ConnState},
218+
{call, quicer, send_dgram, [_, _]},
219+
{ok, _}
220+
) ->
221+
true;
222+
postcondition(
223+
#{state := ConnState},
224+
{call, quicer, send_dgram, [_, _]},
225+
{error, _, _}
226+
) ->
227+
ConnState =/= connected;
228+
postcondition(
229+
#{state := ConnState},
230+
{call, quicer, send_dgram, [_, _]},
231+
{error, _}
232+
) ->
233+
ConnState =/= connected;
201234
postcondition(
202235
#{owner := _, state := connected},
203236
{call, quicer, controlling_process, [_, NewOwner]},
@@ -275,7 +308,8 @@ default_listen_opts() ->
275308
{handshake_idle_timeout_ms, 10000},
276309
% QUIC_SERVER_RESUME_AND_ZERORTT
277310
{server_resumption_level, 2},
278-
{peer_bidi_stream_count, 10}
311+
{peer_bidi_stream_count, 10},
312+
{datagram_receive_enabled, 1}
279313
].
280314

281315
default_conn_opts() ->
@@ -286,7 +320,8 @@ default_conn_opts() ->
286320
{idle_timeout_ms, 0},
287321
{cacertfile, "./msquic/submodules/openssl/test/certs/rootCA.pem"},
288322
{certfile, "./msquic/submodules/openssl/test/certs/servercert.pem"},
289-
{keyfile, "./msquic/submodules/openssl/test/certs/serverkey.pem"}
323+
{keyfile, "./msquic/submodules/openssl/test/certs/serverkey.pem"},
324+
{datagram_receive_enabled, 1}
290325
].
291326

292327
%% Test helpers

test/prop_stateful_server_conn.erl

+36
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ command(#{handle := Handle}) ->
101101
{call, quicer, async_accept_stream, [Handle, ?LET(Opts, quicer_acceptor_opts(), Opts)]}},
102102
{100, {call, quicer, peername, [Handle]}},
103103
{50, {call, quicer, peercert, [Handle]}},
104+
{50, {call, quicer, probe, [Handle, 5000]}},
105+
{50, {call, quicer, send_dgram, [Handle, binary()]}},
104106
{10, {call, quicer, negotiated_protocol, [Handle]}},
105107
{10, {call, quicer, get_connections, []}},
106108
{10, {call, quicer, get_conn_owner, [Handle]}},
@@ -295,6 +297,36 @@ postcondition(#{state := closed}, {call, _Mod, _Fun, _Args}, {error, closed}) ->
295297
postcondition(#{state := accepted}, {call, _Mod, _Fun, _Args}, {error, closed}) ->
296298
%% handshake didnt take place on time
297299
true;
300+
postcondition(
301+
#{state := ConnState},
302+
{call, quicer, probe, [_, _]},
303+
{error, dgram_send_error, _}
304+
) ->
305+
ConnState =/= connected;
306+
postcondition(
307+
#{state := _ConnState},
308+
{call, quicer, probe, [_, _]},
309+
#probe_state{final = FinalState, final_at = FinalTs}
310+
) ->
311+
FinalState =/= undefined andalso FinalTs =/= undefined;
312+
postcondition(
313+
#{state := _ConnState},
314+
{call, quicer, send_dgram, [_, _]},
315+
{ok, _}
316+
) ->
317+
true;
318+
postcondition(
319+
#{state := ConnState},
320+
{call, quicer, send_dgram, [_, _]},
321+
{error, _, _}
322+
) ->
323+
ConnState =/= connected;
324+
postcondition(
325+
#{state := ConnState},
326+
{call, quicer, send_dgram, [_, _]},
327+
{error, _}
328+
) ->
329+
ConnState =/= connected;
298330
postcondition(_State, {call, _Mod, _Fun, _Args} = _Call, _Res) ->
299331
false.
300332

@@ -321,6 +353,10 @@ do_next_state(
321353
#{state := _} = State, ok, {call, quicer, controlling_process, [_, Owner]}
322354
) ->
323355
State#{owner := Owner};
356+
do_next_state(
357+
#{state := _} = State, {error, closed}, {call, _M, _F, _A}
358+
) ->
359+
State#{state := closed};
324360
do_next_state(State, _Res, {call, _Mod, _Fun, _Args}) ->
325361
State.
326362

test/quicer_SUITE.erl

+2-1
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,8 @@ tc_dgram_client_send_fail(_) ->
888888
Opts = default_conn_opts() ++ [{datagram_receive_enabled, 1}],
889889
{ok, Conn} = quicer:async_connect("localhost", 65535, Opts),
890890
?assertEqual(
891-
{error, dgram_send_error, dgram_send_canceled},
891+
%% fire and forget
892+
{ok, 4},
892893
quicer:send_dgram(Conn, <<"ping">>)
893894
),
894895
ok.

test/quicer_connection_SUITE.erl

+10
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,16 @@ tc_closed_conn_reg(_Config) ->
993993
Opts = default_conn_opts() ++ [{quic_registration, ThisReg}],
994994
?assertEqual({error, quic_registration}, quicer:connect("localhost", 443, Opts, 5000)).
995995

996+
tc_conn_probe(_) ->
997+
Opts = default_conn_opts() ++ [{datagram_receive_enabled, 1}],
998+
{ok, Conn} = quicer:async_connect("localhost", 65535, Opts),
999+
?assertMatch(
1000+
#probe_state{final_at = TS, final = ?QUIC_DATAGRAM_SEND_CANCELED} when
1001+
TS =/= undefined,
1002+
quicer:probe(Conn, 5000)
1003+
),
1004+
ok.
1005+
9961006
%%%
9971007
%%% Helpers
9981008
%%%

0 commit comments

Comments
 (0)