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
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 {}.
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+
8898request (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 }}]};
138155idle (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
159177connecting (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 }} ;
189207connected ({call , From }, is_connected , _ ) ->
190208 {keep_state_and_data , [{reply , From , true }]};
191209connected (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 ;
206224handle_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 ]}};
208229handle_common ({call , From }, {request , _ , _ , _ }, State , _Data )
209230when State =/= connected ->
210231 {keep_state_and_data , [{reply , From , {error , disconnected }}]};
@@ -214,13 +235,12 @@ handle_common(cast, {notify, _Method, _Type, _Params}, _State, _Data) ->
214235handle_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 ) ;
224244handle_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
292326conn_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 ;
0 commit comments