Skip to content

Commit ed2cdc8

Browse files
authored
Make NTP client more robust and parametrisable (#61)
* Make NTP client more robust * Add the ntp_servers options ntp_servers and ntp_refresh_period
1 parent 21eb932 commit ed2cdc8

File tree

3 files changed

+151
-106
lines changed

3 files changed

+151
-106
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ In such case the state machine that maintains the connection can be started manu
113113
### `ntp`
114114

115115
An optional NTP client can be started using option `{ntp, true}`.
116-
Such client is disabled by default (`{ntp, false}`), and is not required to authenticate with GRiSP.io. The client sets the time using `grisp_rtems:clock_set/1`
116+
Such client is disabled by default (`{ntp, false}`), and is not required to
117+
authenticate with GRiSP.io. The client sets the time using
118+
`grisp_rtems:clock_set/1`.
119+
The default NTP servers to use can be overridden by setting the `grisp_connect`
120+
option: `{ntp_servers, ["ntp1.server.foo", "ntp2.server.foo"]}`. For every NTP
121+
requests, a server address will be picked randomly from the list.
122+
The default refresh period in seconds can be overridden by setting the
123+
`grisp_connect` option: `{ntp_refresh_period, 1024}`.
117124

118125
### `ws_request_timeout`
119126

src/grisp_connect.app.src

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
{port, 443},
2222
{connect, true}, % keeps a constant connection with grisp.io
2323
{ntp, false}, % if set to true, starts the NTP client
24+
{ntp_servers, ["0.europe.pool.ntp.org"]},
25+
{ntp_refresh_period, 512}, % in seconds
2426
{ws_transport, tls},
2527
{ws_path, "/grisp-connect/ws"},
2628
{ws_request_timeout, 5_000},

src/grisp_connect_ntp.erl

Lines changed: 141 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
-module(grisp_connect_ntp).
22

3-
% API
3+
-behaviour(gen_statem).
4+
5+
-include_lib("kernel/include/logger.hrl").
6+
7+
8+
%--- Exports -------------------------------------------------------------------
9+
10+
% API functions
411
-export([start_link/0]).
512
-export([get_time/0, get_time/1]).
613

7-
-behaviour(gen_statem).
8-
-export([init/1, terminate/3, code_change/4, callback_mode/0, handle_event/4]).
14+
% Behaviour gen_statem callback functions
15+
-export([callback_mode/0]).
16+
-export([init/1]).
917

10-
-define(NTP_PORT, 123). % udp
11-
-define(SERVER_TIMEOUT, 5000). % ms
12-
-define(EPOCH, 2208988800). % offset yr 1900 to unix epochù
13-
-define(RETRY_TIMEOUT, 1000).
18+
% Behaviour gen_statem states callback functions
19+
-export([waiting_ip/3]).
20+
-export([refresh_time/3]).
21+
-export([ready/3]).
1422

15-
-include_lib("kernel/include/logger.hrl").
1623

24+
%--- Types ---------------------------------------------------------------------
25+
26+
-record(data, {
27+
retry_count = 0 :: non_neg_integer()
28+
}).
29+
30+
31+
%--- Macros --------------------------------------------------------------------
32+
33+
-define(NTP_PORT, 123). % NTP's UDP port
34+
-define(EPOCH, 2208988800). % offset yr 1900 to unix epochù
35+
36+
-define(HANDLE_COMMON,
37+
?FUNCTION_NAME(EventType, EventContent, Data) ->
38+
handle_common(EventType, EventContent, ?FUNCTION_NAME, Data)).
1739

1840

19-
% API
41+
%--- API FUNCTIONS -------------------------------------------------------------
2042

2143
start_link() ->
2244
gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -27,82 +49,134 @@ get_time() ->
2749
get_time(Host) ->
2850
gen_statem:call(?MODULE, {?FUNCTION_NAME, Host}).
2951

30-
% gen_statem CALLBACKS ---------------------------------------------------------
3152

32-
init([]) -> {ok, waiting_ip, []}.
53+
%--- BEHAVIOUR gen_statem CALLBACK FUNCTIONS -----------------------------------
3354

34-
terminate(_Reason, _State, _Data) -> ok.
55+
callback_mode() -> [state_functions, state_enter].
3556

36-
code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}.
57+
init([]) -> {ok, waiting_ip, #data{}}.
3758

38-
callback_mode() -> [handle_event_function, state_enter].
3959

40-
%%% STATE CALLBACKS ------------------------------------------------------------
4160

42-
handle_event({call, From}, {get_time, _}, State, Data) when State =/= ready ->
43-
{keep_state, Data, [{reply, From, {error, State}}]};
61+
%--- BEHAVIOUR gen_statem STATES CALLBACK FUNCTIONS -----------------------------
4462

45-
handle_event(enter, _OldState, ready, Data) ->
46-
{keep_state, Data};
47-
handle_event({call, From}, {get_time, Host}, ready, Data) ->
48-
{keep_state, Data, [{reply, From, do_get_time(Host)}]};
49-
50-
handle_event(enter, _OldState, waiting_ip, Data) ->
51-
{next_state, waiting_ip, Data, [{state_timeout, ?RETRY_TIMEOUT, retry}]};
52-
handle_event(state_timeout, retry, waiting_ip, Data) ->
53-
case check_inet_ipv4() of
54-
true ->
55-
?LOG_INFO("ip detected, tryng to contact ntp server..."),
56-
{next_state, waiting_server, Data};
57-
false ->
58-
{next_state, waiting_ip, Data,
59-
[{state_timeout, ?RETRY_TIMEOUT, retry}]}
63+
waiting_ip(enter, _OldState, _Data) ->
64+
% First IP check do not have any delay
65+
{keep_state_and_data, [{state_timeout, 0, check_ip}]};
66+
waiting_ip(state_timeout, check_ip, Data) ->
67+
case grisp_connect_utils:check_inet_ipv4() of
68+
{ok, _IP} -> {next_state, refresh_time, Data};
69+
invalid -> {keep_state_and_data, [{state_timeout, 1000, check_ip}]}
6070
end;
61-
62-
handle_event(enter, _OldState, waiting_server, Data) ->
63-
{next_state, waiting_server, Data,
64-
[{state_timeout, ?RETRY_TIMEOUT, retry}]};
65-
handle_event(state_timeout, retry, waiting_server, Data) ->
66-
try
67-
set_current_time(),
68-
?LOG_INFO("Grisp clock set!"),
69-
{next_state, ready, Data}
70-
catch
71-
Ex:Er ->
72-
?LOG_ERROR("ntp request failed: ~p, ~p",[Ex,Er]),
73-
{next_state, waiting_server, Data,
74-
[{state_timeout, ?RETRY_TIMEOUT, retry}]}
71+
?HANDLE_COMMON.
72+
73+
refresh_time(enter, _OldState, #data{retry_count = RetryCount}) ->
74+
Delay = grisp_connect_utils:retry_delay(RetryCount),
75+
{keep_state_and_data, [{state_timeout, Delay, request_time}]};
76+
refresh_time(state_timeout, request_time,
77+
Data = #data{retry_count = RetryCount}) ->
78+
NTPServer = random_ntp_server(),
79+
case refresh_current_time(NTPServer) of
80+
{error, Reason} ->
81+
?LOG_INFO("Failed to get time from NTP server ~s: ~w",
82+
[NTPServer, Reason]),
83+
{next_state, waiting_ip, Data#data{retry_count = RetryCount + 1}};
84+
{ok, Datetime} ->
85+
?LOG_NOTICE("GRiSP clock set from NTP to ~s",
86+
[format_datetime(Datetime)]),
87+
{next_state, ready, Data#data{retry_count = 0}}
7588
end;
76-
77-
handle_event( E, OldS, NewS, Data) ->
78-
?LOG_WARNING("Unhandled Event = ~p, OldS = ~p, NewS = ~p",[E, OldS, NewS]),
79-
{keep_state, Data}.
80-
81-
% INTERNALS --------------------------------------------------------------------
82-
83-
set_current_time() ->
84-
Time = do_get_time(random_ntp_server()),
85-
RefTS1970 = round(proplists:get_value(receiveTimestamp, (tuple_to_list(Time)))),
86-
CurrSecs = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) + RefTS1970,
87-
CurrDateTime = calendar:gregorian_seconds_to_datetime(CurrSecs),
88-
grisp_rtems:clock_set({CurrDateTime, 0}).
89+
?HANDLE_COMMON.
90+
91+
ready(enter, _OldState, _Data) ->
92+
Period = refresh_period(),
93+
?LOG_DEBUG("Schedule NTP time refresh in ~w seconds", [Period]),
94+
{keep_state_and_data, [{state_timeout, Period * 1000, refresh_time}]};
95+
ready({call, From}, {get_time, Host}, _Data) ->
96+
Reply = case do_get_time(Host) of
97+
{error, _Reason} = Error -> Error;
98+
{ok, Time} -> {ok, Time}
99+
end,
100+
{keep_state_and_data, [{reply, From, Reply}]};
101+
ready(state_timeout, refresh_time, Data) ->
102+
{next_state, refresh_time, Data};
103+
?HANDLE_COMMON.
104+
105+
handle_common({call, _From}, {get_time, _Host}, _State, _Data) ->
106+
{keep_state_and_data, [postpone]};
107+
handle_common({call, From}, Msg, State, _Data) ->
108+
?LOG_WARNING("Unexpected call from ~w in state ~w: ~w",
109+
[From, State, Msg]),
110+
{keep_state_and_data, [{reply, From, {error, unexpected_call}}]};
111+
handle_common(cast, Msg, State, _Data) ->
112+
?LOG_WARNING("Unexpected cast in state ~w: ~w", [State, Msg]),
113+
keep_state_and_data;
114+
handle_common(info, Msg, State, _Data) ->
115+
?LOG_DEBUG("Unexpected message in state ~w: ~w", [State, Msg]),
116+
keep_state_and_data.
117+
118+
119+
%--- INTERNAL FUNCTIONS --------------------------------------------------------
120+
121+
format_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) ->
122+
iolist_to_binary(io_lib:format(
123+
"~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B",
124+
[Year, Month, Day, Hour, Min, Sec]
125+
)).
126+
127+
refresh_current_time(NTPServer) ->
128+
case do_get_time(NTPServer) of
129+
{error, _Reason} = Error -> Error;
130+
{ok, Time} ->
131+
try
132+
RefTS1970 = round(proplists:get_value(receiveTimestamp, (tuple_to_list(Time)))),
133+
CurrSecs = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) + RefTS1970,
134+
CurrDateTime = calendar:gregorian_seconds_to_datetime(CurrSecs),
135+
grisp_rtems:clock_set({CurrDateTime, 0}),
136+
{ok, CurrDateTime}
137+
catch
138+
_:Reason -> {error, Reason}
139+
end
140+
end.
89141

90142
ntp_servers() ->
91-
["0.europe.pool.ntp.org"].
143+
{ok, NTPServers} = application:get_env(grisp_connect, ntp_servers),
144+
NTPServers.
145+
146+
% NTP refresh perido in seconds
147+
refresh_period() ->
148+
{ok, Period} = application:get_env(grisp_connect, ntp_refresh_period),
149+
Period.
92150

93151
random_ntp_server() ->
94152
lists:nth(rand:uniform(length(ntp_servers())), ntp_servers()).
95153

96154
do_get_time(Host) ->
97-
Resp = ntp_request(Host, create_ntp_request()),
98-
process_ntp_response(Resp) .
155+
case ntp_request(Host, create_ntp_request()) of
156+
{error, _Reason} = Error -> Error;
157+
{ok, Resp} ->
158+
try process_ntp_response(Resp) of
159+
Time -> {ok, Time}
160+
catch _:Reason ->
161+
{error, Reason}
162+
end
163+
end.
99164

100165
ntp_request(Host, Binary) ->
101-
{ok, Socket} = gen_udp:open(0, [binary, {active, false}]),
102-
gen_udp:send(Socket, Host, ?NTP_PORT, Binary),
103-
{ok, {_Address, _Port, Resp}} = gen_udp:recv(Socket, 0, 500),
104-
gen_udp:close(Socket),
105-
Resp.
166+
case gen_udp:open(0, [binary, {active, false}]) of
167+
{error, _Reason} = Error -> Error;
168+
{ok, Socket} ->
169+
try gen_udp:send(Socket, Host, ?NTP_PORT, Binary) of
170+
{error, _Reason} = Error -> Error;
171+
ok ->
172+
case gen_udp:recv(Socket, 0, 500) of
173+
{error, _Reason} = Error -> Error;
174+
{ok, {_Address, _Port, Resp}} -> {ok, Resp}
175+
end
176+
after
177+
gen_udp:close(Socket)
178+
end
179+
end.
106180

107181
process_ntp_response(Ntp_response) ->
108182
<<LI:2, Version:3, Mode:3, Stratum:8, Poll:8/signed, Precision:8/signed,
@@ -131,41 +205,3 @@ binfrac(0, _, Frac) ->
131205
Frac;
132206
binfrac(Bin, N, Frac) ->
133207
binfrac(Bin bsr 1, N*2, Frac + (Bin band 1)/N).
134-
135-
% INET IP CHECK UTILS ----------------------------------------------------------
136-
137-
check_inet_ipv4() ->
138-
case get_ip_of_valid_interfaces() of
139-
{_,_,_,_} = IP when IP =/= {127,0,0,1} -> true;
140-
_ -> false
141-
end.
142-
143-
get_ipv4_from_opts([]) ->
144-
undefined;
145-
get_ipv4_from_opts([{addr, {_1, _2, _3, _4}} | _]) ->
146-
{_1, _2, _3, _4};
147-
get_ipv4_from_opts([_ | TL]) ->
148-
get_ipv4_from_opts(TL).
149-
150-
has_ipv4(Opts) ->
151-
get_ipv4_from_opts(Opts) =/= undefined.
152-
153-
flags_are_ok(Flags) ->
154-
lists:member(up, Flags) and
155-
lists:member(running, Flags) and
156-
not lists:member(loopback, Flags).
157-
158-
get_valid_interfaces() ->
159-
{ok, Interfaces} = inet:getifaddrs(),
160-
[
161-
Opts
162-
|| {_Name, [{flags, Flags} | Opts]} <- Interfaces,
163-
flags_are_ok(Flags),
164-
has_ipv4(Opts)
165-
].
166-
167-
get_ip_of_valid_interfaces() ->
168-
case get_valid_interfaces() of
169-
[Opts | _] -> get_ipv4_from_opts(Opts);
170-
_ -> undefined
171-
end.

0 commit comments

Comments
 (0)