From 3821d145e7d4490982c740a18e7b066975621733 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 19 Jan 2024 17:37:19 +0100 Subject: [PATCH 1/6] Add gen_statem to automatically connect to grisp.io on boot --- src/grisp_io_connection.erl | 123 ++++++++++++++++++++++++++++++++++++ src/grisp_seawater.app.src | 3 +- src/grisp_seawater_sup.erl | 5 +- src/grisp_seawater_ws.erl | 1 + 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/grisp_io_connection.erl diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl new file mode 100644 index 0000000..a822841 --- /dev/null +++ b/src/grisp_io_connection.erl @@ -0,0 +1,123 @@ +%% @doc State machine to ensure connectivity with grisp.io +-module(grisp_io_connection). + +% API +-export([start_link/0]). +-export([connect/0]). + +-behaviour(gen_statem). +-export([init/1, terminate/3, code_change/4, callback_mode/0, handle_event/4]). + +-include_lib("kernel/include/logger.hrl"). + +-define(STD_TIMEOUT, 1000). + +% API + +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +connect() -> + gen_statem:cast(?MODULE, ?FUNCTION_NAME). + +% gen_statem CALLBACKS --------------------------------------------------------- + +init([]) -> + {ok, Connect} = application:get_env(grisp_seawater, connect), + NextState = case Connect of + true -> waiting_ip; + false -> idle + end, + {ok, NextState, []}. + +terminate(_Reason, _State, _Data) -> ok. + +code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. + +callback_mode() -> [handle_event_function, state_enter]. + +%%% STATE CALLBACKS ------------------------------------------------------------ + +% IDLE +handle_event(enter, _OldState, idle, _Data) -> + keep_state_and_data; +handle_event(cast, connect, idle, Data) -> + {next_state, waiting_ip, Data}; + +% WAITING_IP +handle_event(enter, _OldState, waiting_ip, Data) -> + {next_state, waiting_ip, Data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, waiting_ip, Data) -> + case check_inet_ipv4() of + {ok, IP} -> + ?LOG_INFO("Detected IP: ~p", [IP]), + {next_state, connecting, Data}; + invalid -> + ?LOG_INFO("Waiting IP..."), + {next_state, waiting_ip, Data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% CONNECTING +handle_event(enter, _OldState, connecting, _Data) -> + {ok, Domain} = application:get_env(grisp_seawater, seawater_domain), + {ok, Port} = application:get_env(grisp_seawater, seawater_port), + ?LOG_NOTICE("Connecting to ~p:~p",[Domain, Port]), + {keep_state_and_data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, connecting, Data) -> + case grisp_seawater_client:connect() of + ok -> + ?LOG_NOTICE("Connection enstablished!"), + {next_state, connected, Data}; + Error -> + ?LOG_ERROR("Connection failed with error ~p, Retryng ...",[Error]), + {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% CONNECTED +handle_event(enter, _OldState, connected, _Data) -> + keep_state_and_data; +handle_event(info, disconnected, connected, Data) -> + ?LOG_WARNING("Disconnected!"), + {next_state, waiting_ip, Data}; + +handle_event(E, OldS, NewS, Data) -> + ?LOG_ERROR("Unhandled Event = ~p, OldS = ~p, NewS = ~p",[E, OldS, NewS]), + {keep_state, Data}. + +% INTERNALS -------------------------------------------------------------------- + +check_inet_ipv4() -> + case get_ip_of_valid_interfaces() of + {_,_,_,_} = IP when IP =/= {127,0,0,1} -> {ok, IP}; + _ -> invalid + end. + +get_ipv4_from_opts([]) -> + undefined; +get_ipv4_from_opts([{addr, {_1, _2, _3, _4}} | _]) -> + {_1, _2, _3, _4}; +get_ipv4_from_opts([_ | TL]) -> + get_ipv4_from_opts(TL). + +has_ipv4(Opts) -> + get_ipv4_from_opts(Opts) =/= undefined. + +flags_are_ok(Flags) -> + lists:member(up, Flags) and + lists:member(running, Flags) and + not lists:member(loopback, Flags). + +get_valid_interfaces() -> + {ok, Interfaces} = inet:getifaddrs(), + [ + Opts + || {_Name, [{flags, Flags} | Opts]} <- Interfaces, + flags_are_ok(Flags), + has_ipv4(Opts) + ]. + +get_ip_of_valid_interfaces() -> + case get_valid_interfaces() of + [Opts | _] -> get_ipv4_from_opts(Opts); + _ -> undefined + end. diff --git a/src/grisp_seawater.app.src b/src/grisp_seawater.app.src index 76eff09..062924a 100644 --- a/src/grisp_seawater.app.src +++ b/src/grisp_seawater.app.src @@ -16,8 +16,9 @@ {env, [ {seawater_domain, "grisp.io"}, {seawater_port, 7777}, + {connect, true}, % keeps a constant connection with grisp.io {ntp, false}, % if set to true, starts the NTP client - {ws_requests_timeout, 5_000} + {ws_requests_timeout, 5_000}, ]}, {modules, []}, {links, []} diff --git a/src/grisp_seawater_sup.erl b/src/grisp_seawater_sup.erl index a045584..5eeeac4 100644 --- a/src/grisp_seawater_sup.erl +++ b/src/grisp_seawater_sup.erl @@ -38,7 +38,10 @@ init([]) -> {ok, true} -> [worker(grisp_seawater_ntp, [])]; {ok, false} -> [] end, - ChildSpecs = NTP ++ [worker(grisp_seawater_ws, [])], + ChildSpecs = NTP ++ [ + worker(grisp_seawater_ws, []), + worker(grisp_io_connection, []) + ], {ok, {SupFlags, ChildSpecs}}. %% internal functions diff --git a/src/grisp_seawater_ws.erl b/src/grisp_seawater_ws.erl index 886784c..022bae5 100644 --- a/src/grisp_seawater_ws.erl +++ b/src/grisp_seawater_ws.erl @@ -124,6 +124,7 @@ handle_info({gun_down, Pid, ws, closed, [Stream]}, erlang:cancel_timer(Tref), gen_server:reply(Caller, {error, ws_closed}) end || {Caller, Tref} <- maps:values(Requests)], + grisp_io_connection ! disconnected, {noreply, shutdown_gun(S#state{requests = #{}})}; handle_info(M, S) -> ?LOG_WARNING(#{event => unhandled_info, info => M}), From ad5ad35a28edf3edd99ea246ea286c97e5db3fb4 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Thu, 25 Jan 2024 18:21:29 +0100 Subject: [PATCH 2/6] Use gen_server:cast instead of sending a cross-module message --- src/grisp_io_connection.erl | 14 ++++++++++---- src/grisp_seawater_ws.erl | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl index a822841..cc4e689 100644 --- a/src/grisp_io_connection.erl +++ b/src/grisp_io_connection.erl @@ -1,10 +1,13 @@ %% @doc State machine to ensure connectivity with grisp.io -module(grisp_io_connection). -% API +% External API -export([start_link/0]). -export([connect/0]). +% Internal API +-export([disconnected/0]). + -behaviour(gen_statem). -export([init/1, terminate/3, code_change/4, callback_mode/0, handle_event/4]). @@ -20,6 +23,9 @@ start_link() -> connect() -> gen_statem:cast(?MODULE, ?FUNCTION_NAME). +disconnected() -> + gen_statem:castl(?MODULE, ?FUNCTION_NAME). + % gen_statem CALLBACKS --------------------------------------------------------- init([]) -> @@ -66,7 +72,7 @@ handle_event(enter, _OldState, connecting, _Data) -> handle_event(state_timeout, retry, connecting, Data) -> case grisp_seawater_client:connect() of ok -> - ?LOG_NOTICE("Connection enstablished!"), + ?LOG_NOTICE(#{event => connected}), {next_state, connected, Data}; Error -> ?LOG_ERROR("Connection failed with error ~p, Retryng ...",[Error]), @@ -76,8 +82,8 @@ handle_event(state_timeout, retry, connecting, Data) -> % CONNECTED handle_event(enter, _OldState, connected, _Data) -> keep_state_and_data; -handle_event(info, disconnected, connected, Data) -> - ?LOG_WARNING("Disconnected!"), +handle_event(cast, disconnected, connected, Data) -> + ?LOG_WARNING(#{event => disconnected}), {next_state, waiting_ip, Data}; handle_event(E, OldS, NewS, Data) -> diff --git a/src/grisp_seawater_ws.erl b/src/grisp_seawater_ws.erl index 022bae5..efb6daf 100644 --- a/src/grisp_seawater_ws.erl +++ b/src/grisp_seawater_ws.erl @@ -124,7 +124,7 @@ handle_info({gun_down, Pid, ws, closed, [Stream]}, erlang:cancel_timer(Tref), gen_server:reply(Caller, {error, ws_closed}) end || {Caller, Tref} <- maps:values(Requests)], - grisp_io_connection ! disconnected, + grisp_io_connection:disconnected(), {noreply, shutdown_gun(S#state{requests = #{}})}; handle_info(M, S) -> ?LOG_WARNING(#{event => unhandled_info, info => M}), From af515e3b1c3c656e6ad7b78f89f4c9ed286c6ec9 Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 26 Jan 2024 09:08:13 +0100 Subject: [PATCH 3/6] Match the generic range 127.0.0.0/8 to identify the loopback IP --- src/grisp_io_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl index cc4e689..9aad4cf 100644 --- a/src/grisp_io_connection.erl +++ b/src/grisp_io_connection.erl @@ -94,7 +94,7 @@ handle_event(E, OldS, NewS, Data) -> check_inet_ipv4() -> case get_ip_of_valid_interfaces() of - {_,_,_,_} = IP when IP =/= {127,0,0,1} -> {ok, IP}; + {IP1,_,_,_} = IP when IP1 =/= 127 -> {ok, IP}; _ -> invalid end. From 5a4cb4fea34996b3b3ec89f233967cb8f800d64c Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 26 Jan 2024 09:21:04 +0100 Subject: [PATCH 4/6] Use structured logs --- src/grisp_io_connection.erl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl index 9aad4cf..fef683a 100644 --- a/src/grisp_io_connection.erl +++ b/src/grisp_io_connection.erl @@ -56,10 +56,10 @@ handle_event(enter, _OldState, waiting_ip, Data) -> handle_event(state_timeout, retry, waiting_ip, Data) -> case check_inet_ipv4() of {ok, IP} -> - ?LOG_INFO("Detected IP: ~p", [IP]), + ?LOG_INFO(#{event => checked_ip, ip => IP}), {next_state, connecting, Data}; invalid -> - ?LOG_INFO("Waiting IP..."), + ?LOG_INFO(#{event => waiting_ip}), {next_state, waiting_ip, Data, [{state_timeout, ?STD_TIMEOUT, retry}]} end; @@ -67,7 +67,7 @@ handle_event(state_timeout, retry, waiting_ip, Data) -> handle_event(enter, _OldState, connecting, _Data) -> {ok, Domain} = application:get_env(grisp_seawater, seawater_domain), {ok, Port} = application:get_env(grisp_seawater, seawater_port), - ?LOG_NOTICE("Connecting to ~p:~p",[Domain, Port]), + ?LOG_NOTICE(#{event => connecting, domain => Domain, port => Port}), {keep_state_and_data, [{state_timeout, 0, retry}]}; handle_event(state_timeout, retry, connecting, Data) -> case grisp_seawater_client:connect() of @@ -75,7 +75,8 @@ handle_event(state_timeout, retry, connecting, Data) -> ?LOG_NOTICE(#{event => connected}), {next_state, connected, Data}; Error -> - ?LOG_ERROR("Connection failed with error ~p, Retryng ...",[Error]), + ?LOG_ERROR(#{event => connection_failed, + reason => Error}), {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, retry}]} end; @@ -87,7 +88,10 @@ handle_event(cast, disconnected, connected, Data) -> {next_state, waiting_ip, Data}; handle_event(E, OldS, NewS, Data) -> - ?LOG_ERROR("Unhandled Event = ~p, OldS = ~p, NewS = ~p",[E, OldS, NewS]), + ?LOG_ERROR(#{event => unhandled_gen_statem_event, + gen_statem_event => E, + old_state => OldS, + new_state => NewS}), {keep_state, Data}. % INTERNALS -------------------------------------------------------------------- From 1b6d77dab48e75137596e759807890d258c443dd Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 26 Jan 2024 12:30:29 +0100 Subject: [PATCH 5/6] Fix rebase --- src/grisp_io_connection.erl | 10 +++++----- src/grisp_seawater.app.src | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl index fef683a..ae8af47 100644 --- a/src/grisp_io_connection.erl +++ b/src/grisp_io_connection.erl @@ -68,15 +68,15 @@ handle_event(enter, _OldState, connecting, _Data) -> {ok, Domain} = application:get_env(grisp_seawater, seawater_domain), {ok, Port} = application:get_env(grisp_seawater, seawater_port), ?LOG_NOTICE(#{event => connecting, domain => Domain, port => Port}), + grisp_seawater_ws:connect(Domain, Port), {keep_state_and_data, [{state_timeout, 0, retry}]}; handle_event(state_timeout, retry, connecting, Data) -> - case grisp_seawater_client:connect() of - ok -> + case grisp_seawater_ws:is_connected() of + true -> ?LOG_NOTICE(#{event => connected}), {next_state, connected, Data}; - Error -> - ?LOG_ERROR(#{event => connection_failed, - reason => Error}), + false -> + ?LOG_NOTICE(#{event => waiting_ws_connection}), {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, retry}]} end; diff --git a/src/grisp_seawater.app.src b/src/grisp_seawater.app.src index 062924a..af9ba50 100644 --- a/src/grisp_seawater.app.src +++ b/src/grisp_seawater.app.src @@ -18,7 +18,7 @@ {seawater_port, 7777}, {connect, true}, % keeps a constant connection with grisp.io {ntp, false}, % if set to true, starts the NTP client - {ws_requests_timeout, 5_000}, + {ws_requests_timeout, 5_000} ]}, {modules, []}, {links, []} From 365ff0b677ad653c489afb3e76a2beac2d34a01c Mon Sep 17 00:00:00 2001 From: Luca Succi Date: Fri, 26 Jan 2024 12:41:03 +0100 Subject: [PATCH 6/6] Document connect option --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 497b276..eaff1e4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Grisp2 Seawater Client Library ## Application env options +### connect + +This option is set to `true` as default. Set it to `false` to prevent automatic connection to GRiSP.io on boot. +In such case the state machine that maintains the connection can be started manually using `grisp_io_connection:connect()`. + ### ntp An optional NTP client can be started using option `{ntp, true}`.