Skip to content

Commit 062f320

Browse files
authored
Add grisp-io protocol version handshake (#53)
- Remove invalid configuration. - Fix dialyzer spec. - Add option to limit the number of connection retries, mostly for testing use-cases that will always fail to connect. - Implemente wait_connected in the client to support reaching maximum connection retries and get the last known error.
1 parent ac8da97 commit 062f320

12 files changed

+147
-57
lines changed

config/dev.config

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
[
22
{grisp_cryptoauth, [
3-
{tls_server_trusted_certs_cb, []},
43
{tls_server_trusted_certs, {priv, grisp_connect, "server"}}
54
]},
65

config/local.config

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
]},
55

66
{grisp_cryptoauth, [
7-
{tls_server_trusted_certs_cb, []},
87
{tls_server_trusted_certs, {priv, grisp_connect, "server"}},
98
{tls_client_trusted_certs, {test, grisp_connect, "certs/CA.crt"}},
109
{client_certs, {test, grisp_connect, "certs/client.crt"}},

config/test.config

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
]},
55

66
{grisp_cryptoauth, [
7-
{tls_server_trusted_certs_cb, []},
87
{tls_client_trusted_certs, {test, grisp_connect, "certs/CA.crt"}},
98
{client_certs, {test, grisp_connect, "certs/client.crt"}},
109
{client_key, {test, grisp_connect, "certs/client.key"}},

rebar.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{<<"gun">>,{pkg,<<"gun">>,<<"2.1.0">>},1},
77
{<<"jarl">>,
88
{git,"https://github.com/grisp/jarl.git",
9-
{ref,"10085d38df19c67664d33ef61f515c92a8b0de56"}},
9+
{ref,"24d53cc7b521b126588be1f36afecd1d4eb59db3"}},
1010
0},
1111
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
1212
{<<"mapz">>,{pkg,<<"mapz">>,<<"2.4.0">>},1}]}.

src/grisp_connect.app.src

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
{ws_path, "/grisp-connect/ws"},
2626
{ws_request_timeout, 5_000},
2727
{ws_ping_timeout, 60_000},
28+
{ws_max_retries, infinity},
2829
{logs_interval, 2_000},
2930
{logs_batch_size, 100},
3031
{logger, [

src/grisp_connect_api.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
% @doc Handles requests and notifications from grisp.io.
1616
-spec handle_msg(Msg) ->
1717
ok | {reply, Result :: term(), ReqRef :: binary() | integer()}
18+
| {error, Code :: integer() | atom(), Message :: binary() | undefined, ErData :: term(), ReqRef :: binary() | integer()}
1819
when Msg :: {request, Method :: jarl:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
1920
| {notification, jarl:method(), Params :: map() | list()}.
2021
handle_msg({notification, M, Params}) ->

src/grisp_connect_client.erl

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
-export([start_link/0]).
1414
-export([connect/0]).
1515
-export([is_connected/0]).
16+
-export([wait_connected/1]).
1617
-export([request/3]).
1718
-export([notify/3]).
1819

@@ -37,7 +38,10 @@
3738
ws_path :: binary(),
3839
ws_transport :: tcp | tls,
3940
conn :: undefined | pid(),
40-
retry_count = 0 :: non_neg_integer()
41+
retry_count = 0 :: non_neg_integer(),
42+
last_error :: term(),
43+
max_retries = infinity :: non_neg_integer() | infinity,
44+
wait_calls = [] :: [gen_statem:from()]
4145
}).
4246

4347
-type data() :: #data{}.
@@ -50,6 +54,7 @@
5054

5155
%--- Macros --------------------------------------------------------------------
5256

57+
-define(GRISP_IO_PROTOCOL, <<"grisp-io-v1">>).
5358
-define(FORMAT(FMT, ARGS), iolist_to_binary(io_lib:format(FMT, ARGS))).
5459
-define(STD_TIMEOUT, 1000).
5560
-define(CONNECT_TIMEOUT, 5000).
@@ -85,6 +90,11 @@ is_connected() ->
8590
catch exit:noproc -> false
8691
end.
8792

93+
wait_connected(Timeout) ->
94+
try gen_statem:call(?MODULE, ?FUNCTION_NAME, Timeout)
95+
catch exit:noproc -> {error, noproc}
96+
end.
97+
8898
request(Method, Type, Params) ->
8999
gen_statem:call(?MODULE, {?FUNCTION_NAME, Method, Type, Params}).
90100

@@ -107,11 +117,13 @@ init([]) ->
107117
Port = ?ENV(port, is_integer(V) andalso V >= 0 andalso V < 65536),
108118
WsTransport = ?ENV(ws_transport, V =:= tls orelse V =:= tcp),
109119
WsPath = ?ENV(ws_path, is_binary(V) orelse is_list(V), as_bin(V)),
120+
MaxRetries = ?ENV(ws_max_retries, is_integer(V) orelse V =:= infinity),
110121
Data = #data{
111122
domain = Domain,
112123
port = Port,
113124
ws_transport = WsTransport,
114-
ws_path = WsPath
125+
ws_path = WsPath,
126+
max_retries = MaxRetries
115127
},
116128
% The error list is put in a persistent term to not add noise to the state.
117129
persistent_term:put({?MODULE, self()}, generic_errors()),
@@ -133,8 +145,13 @@ callback_mode() -> [state_functions, state_enter].
133145

134146
%--- Behaviour gen_statem State Callback Functions -----------------------------
135147

136-
idle(enter, _OldState, _Data) ->
137-
keep_state_and_data;
148+
idle(enter, _OldState,
149+
Data = #data{wait_calls = WaitCalls, last_error = LastError}) ->
150+
% When entering idle, we reply to all wait_connected calls with the last error
151+
gen_statem:reply([{reply, F, {error, LastError}} || F <- WaitCalls]),
152+
{keep_state, Data#data{wait_calls = [], last_error = undefined}};
153+
idle({call, From}, wait_connected, _) ->
154+
{keep_state_and_data, [{reply, From, {error, not_connecting}}]};
138155
idle(cast, connect, Data) ->
139156
{next_state, waiting_ip, Data};
140157
?HANDLE_COMMON.
@@ -152,14 +169,14 @@ waiting_ip(state_timeout, retry, Data = #data{retry_count = RetryCount}) ->
152169
{next_state, connecting, Data};
153170
invalid ->
154171
?LOG_DEBUG(#{event => waiting_ip}),
155-
{repeat_state, Data#data{retry_count = RetryCount + 1}}
172+
{repeat_state, Data#data{retry_count = RetryCount + 1,
173+
last_error = no_ip_available}}
156174
end;
157175
?HANDLE_COMMON.
158176

159177
connecting(enter, _OldState, Data) ->
160178
{keep_state, Data, [{state_timeout, 0, connect}]};
161-
connecting(state_timeout, connect,
162-
Data = #data{conn = undefined, retry_count = RetryCount}) ->
179+
connecting(state_timeout, connect, Data = #data{conn = undefined}) ->
163180
?LOG_INFO(#{description => <<"Connecting to grisp.io">>,
164181
event => connecting}),
165182
case conn_start(Data) of
@@ -168,24 +185,25 @@ connecting(state_timeout, connect,
168185
{error, Reason} ->
169186
?LOG_WARNING("Failed to connect to grisp.io: ~p", [Reason],
170187
#{event => connection_failed, reason => Reason}),
171-
{next_state, waiting_ip, Data#data{retry_count = RetryCount + 1}}
188+
reconnect(Data, Reason)
172189
end;
173-
connecting(state_timeout, timeout, Data = #data{retry_count = RetryCount}) ->
190+
connecting(state_timeout, timeout, Data) ->
174191
Reason = connect_timeout,
175192
?LOG_WARNING(#{description => <<"Timeout while connecting to grisp.io">>,
176193
event => connection_failed, reason => Reason}),
177-
Data2 = conn_close(Data, Reason),
178-
{next_state, waiting_ip, Data2#data{retry_count = RetryCount + 1}};
179-
connecting(info, {jarl, Conn, connected}, Data = #data{conn = Conn}) ->
194+
reconnect(conn_close(Data, Reason), Reason);
195+
connecting(info, {jarl, Conn, {connected, _}}, Data = #data{conn = Conn}) ->
180196
% Received from the connection process
181197
?LOG_NOTICE(#{description => <<"Connected to grisp.io">>,
182198
event => connected}),
183199
{next_state, connected, Data#data{retry_count = 0}};
184200
?HANDLE_COMMON.
185201

186-
connected(enter, _OldState, _Data) ->
202+
connected(enter, _OldState, Data = #data{wait_calls = WaitCalls}) ->
203+
% When entering connected, we reply to all wait_connected calls with ok
204+
gen_statem:reply([{reply, F, ok} || F <- WaitCalls]),
187205
grisp_connect_log_server:start(),
188-
keep_state_and_data;
206+
{keep_state, Data#data{wait_calls = [], last_error = undefined}};
189207
connected({call, From}, is_connected, _) ->
190208
{keep_state_and_data, [{reply, From, true}]};
191209
connected(info, {jarl, Conn, Msg}, Data = #data{conn = Conn}) ->
@@ -205,6 +223,9 @@ handle_common(cast, connect, State, _Data) when State =/= idle ->
205223
keep_state_and_data;
206224
handle_common({call, From}, is_connected, State, _) when State =/= connected ->
207225
{keep_state_and_data, [{reply, From, false}]};
226+
handle_common({call, From}, wait_connected, _State,
227+
Data = #data{wait_calls = WaitCalls}) ->
228+
{keep_state, Data#data{wait_calls = [From | WaitCalls]}};
208229
handle_common({call, From}, {request, _, _, _}, State, _Data)
209230
when State =/= connected ->
210231
{keep_state_and_data, [{reply, From, {error, disconnected}}]};
@@ -214,13 +235,12 @@ handle_common(cast, {notify, _Method, _Type, _Params}, _State, _Data) ->
214235
handle_common(info, reboot, _, _) ->
215236
init:stop(),
216237
keep_state_and_data;
217-
handle_common(info, {'EXIT', Conn, Reason}, _State,
218-
Data = #data{conn = Conn, retry_count = RetryCount}) ->
238+
handle_common(info, {'EXIT', Conn, Reason}, _State, Data = #data{conn = Conn}) ->
219239
% The connection process died
220240
?LOG_WARNING(#{description =>
221241
?FORMAT("The connection to grisp.io died: ~p", [Reason]),
222242
event => connection_failed, reason => Reason}),
223-
{next_state, waiting_ip, conn_died(Data#data{retry_count = RetryCount + 1})};
243+
reconnect(conn_died(Data), Reason);
224244
handle_common(info, {'EXIT', _Conn, _Reason}, _State, _Data) ->
225245
% Ignore any EXIT from past jarl connections
226246
keep_state_and_data;
@@ -287,6 +307,20 @@ handle_connection_message(Data, Msg) ->
287307
keep_state_and_data
288308
end.
289309

310+
reconnect(Data = #data{retry_count = RetryCount,
311+
max_retries = MaxRetries,
312+
last_error = LastError}, Reason)
313+
when MaxRetries =/= infinity, RetryCount > MaxRetries ->
314+
Error = case Reason of undefined -> LastError; E -> E end,
315+
?LOG_ERROR(#{description => <<"Max retries reached, giving up connecting to grisp.io">>,
316+
event => max_retries_reached, last_error => LastError}),
317+
{next_state, idle, Data#data{retry_count = 0, last_error = Error}};
318+
reconnect(Data = #data{retry_count = RetryCount,
319+
last_error = LastError}, Reason) ->
320+
Error = case Reason of undefined -> LastError; E -> E end,
321+
{next_state, waiting_ip,
322+
Data#data{retry_count = RetryCount + 1, last_error = Error}}.
323+
290324
% Connection Functions
291325

292326
conn_start(Data = #data{conn = undefined,
@@ -308,7 +342,8 @@ conn_start(Data = #data{conn = undefined,
308342
path => WsPath,
309343
errors => ErrorList,
310344
ping_timeout => WsPingTimeout,
311-
request_timeout => WsReqTimeout
345+
request_timeout => WsReqTimeout,
346+
protocols => [?GRISP_IO_PROTOCOL]
312347
},
313348
case jarl:start_link(self(), ConnOpts) of
314349
{error, _Reason} = Error -> Error;

test/grisp_connect_api_SUITE.erl

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ all() ->
3232
lists:suffix("_test", atom_to_list(F))
3333
].
3434

35-
init_per_suite(Config) ->
36-
CertDir = cert_dir(),
37-
Apps = grisp_connect_test_server:start(CertDir),
38-
[{apps, Apps} | Config].
39-
40-
end_per_suite(Config) ->
41-
grisp_connect_test_server:stop(?config(apps, Config)).
42-
35+
init_per_testcase(TestCase, Config)
36+
when TestCase =:= bad_client_version_test;
37+
TestCase =:= bad_server_version_test ->
38+
Config;
4339
init_per_testcase(TestCase, Config) ->
40+
CertDir = cert_dir(),
41+
Apps = grisp_connect_test_server:start(#{cert_dir => CertDir}),
4442
{ok, _} = application:ensure_all_started(grisp_emulation),
4543
{ok, _} = application:ensure_all_started(grisp_connect),
4644
case TestCase of
@@ -49,16 +47,66 @@ init_per_testcase(TestCase, Config) ->
4947
?assertEqual(ok, wait_connection()),
5048
grisp_connect_test_server:listen()
5149
end,
52-
Config.
50+
[{apps, Apps} | Config].
5351

52+
end_per_testcase(TestCase, Config)
53+
when TestCase =:= bad_client_version_test;
54+
TestCase =:= bad_server_version_test ->
55+
Config;
5456
end_per_testcase(_, Config) ->
5557
ok = application:stop(grisp_connect),
5658
grisp_connect_test_server:wait_disconnection(),
5759
?assertEqual([], flush()),
60+
grisp_connect_test_server:stop(proplists:get_value(apps, Config)),
5861
Config.
5962

6063
%--- Tests ---------------------------------------------------------------------
6164

65+
bad_client_version_test(_) ->
66+
CertDir = cert_dir(),
67+
Apps = grisp_connect_test_server:start(#{
68+
cert_dir => CertDir,
69+
expected_protocol => <<"grisp-io-v42">>}),
70+
try
71+
{ok, _} = application:ensure_all_started(grisp_emulation),
72+
application:load(grisp_connect),
73+
application:set_env(grisp_connect, ws_max_retries, 2),
74+
{ok, _} = application:ensure_all_started(grisp_connect),
75+
try
76+
?assertMatch({error, ws_upgrade_failed}, wait_connection())
77+
after
78+
ok = application:stop(grisp_connect)
79+
end
80+
after
81+
grisp_connect_test_server:wait_disconnection(),
82+
?assertEqual([], flush()),
83+
grisp_connect_test_server:stop(Apps)
84+
end,
85+
ok.
86+
87+
bad_server_version_test(_) ->
88+
CertDir = cert_dir(),
89+
Apps = grisp_connect_test_server:start(#{
90+
cert_dir => CertDir,
91+
selected_protocol => <<"grisp-io-v42">>}),
92+
try
93+
{ok, _} = application:ensure_all_started(grisp_emulation),
94+
application:load(grisp_connect),
95+
application:set_env(grisp_connect, ws_max_retries, 2),
96+
{ok, _} = application:ensure_all_started(grisp_connect),
97+
try
98+
% There is no way to know the reason why gun closed the connection
99+
?assertMatch({error, {closed, _}}, wait_connection())
100+
after
101+
ok = application:stop(grisp_connect)
102+
end
103+
after
104+
grisp_connect_test_server:wait_disconnection(),
105+
?assertEqual([], flush()),
106+
grisp_connect_test_server:stop(Apps)
107+
end,
108+
ok.
109+
62110
auto_connect_test(_) ->
63111
?assertMatch(ok, wait_connection()).
64112

test/grisp_connect_log_SUITE.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ all() ->
2828

2929
init_per_suite(Config) ->
3030
CertDir = cert_dir(),
31-
Apps = grisp_connect_test_server:start(CertDir),
31+
Apps = grisp_connect_test_server:start(#{cert_dir => CertDir}),
3232
[{apps, Apps} | Config].
3333

3434
end_per_suite(Config) ->

test/grisp_connect_reconnect_SUITE.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ end_per_suite(Config) ->
3636
[?assertEqual(ok, application:stop(App)) || App <- ?config(apps, Config)].
3737

3838
init_per_testcase(_, Config) ->
39-
start_cowboy(cert_dir()),
39+
start_cowboy(#{cert_dir => cert_dir()}),
4040
{ok, _} = application:ensure_all_started(grisp_emulation),
4141
application:set_env(grisp_connect, ws_ping_timeout, 120_000),
4242
{ok, _} = application:ensure_all_started(grisp_connect),
@@ -62,7 +62,7 @@ reconnect_on_disconnection_test(Config) ->
6262
?assertMatch(ok, wait_connection()),
6363
stop_cowboy(),
6464
?assertMatch(ok, wait_disconnection()),
65-
start_cowboy(cert_dir()),
65+
start_cowboy(#{cert_dir => cert_dir()}),
6666
?assertMatch(ok, wait_connection(1200)),
6767
Config.
6868

@@ -88,7 +88,7 @@ reconnect_on_closed_frame_test(_) ->
8888
%--- Internal Functions --------------------------------------------------------
8989

9090
connection_gun_pid() ->
91-
{_, {data, _, _, _, _, ConnPid, _}} = sys:get_state(grisp_connect_client),
91+
{_, {data, _, _, _, _, ConnPid, _, _, _, _}} = sys:get_state(grisp_connect_client),
9292
% Depends on the internal state of jarl_connection
93-
{_, {data, _, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _}} = sys:get_state(ConnPid),
93+
{_, {data, _, _, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _}} = sys:get_state(ConnPid),
9494
GunPid.

0 commit comments

Comments
 (0)