I just started learning Erlang and this is a module from a test project of mine. I'm doing it so that I can understand a little better how the supervision tree works, to practice fail-fast code and some programming best practices.
The udp_listener
process listens for UDP messages. It's role is to listen to communication requests from other hosts in the network and contact them through TCP using the port number defined in the UDP message.
The handle_info(...)
function is called every time an UDP message is received by the socket, it decodes the UDP message and passes it to the tcp_client
process.
From what I understood the only failure point in my code is the decode_udp_message(Data)
called sometime inside handle_info(...)
.
When this functions fails, is the whole udp_listener
process is restarted? Should I keep this from happening?
Shouldn't just the handle_info(...)
function silently die without affecting the udp_listener
process?
How should I log an exception on decode_udp_message(Data)
? I would like to register somewhere the host and it's failed message.
-module(udp_listener).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
%% ====================================================================
%% API functions
%% ====================================================================
-export([start_link/1]).
start_link(Port) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Port, []).
%% ====================================================================
%% Behavioural functions
%% ====================================================================
%% init/1
%% ====================================================================
-spec init(Port :: non_neg_integer()) -> Result when
Result :: {ok, Socket :: port()}
| {stop, Reason :: term()}.
%% ====================================================================
init(Port) ->
SocketTuple = gen_udp:open(Port, [binary, {active, true}]),
case SocketTuple of
{ok, Socket} -> {ok, Socket};
{error, eaddrinuse} -> {stop, udp_port_in_use};
{error, Reason} -> {stop, Reason}
end.
% Handles "!" messages from the socket
handle_info({udp, Socket, Host, _Port, Data}, State) -> Socket = State,
handle_ping(Host, Data),
{noreply, Socket}.
terminate(_Reason, State) -> Socket = State,
gen_udp:close(Socket).
handle_cast(_Request, State) -> {noreply, State}.
handle_call(_Request, _From, State) -> {noreply, State}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%% ====================================================================
%% Internal functions
%% ====================================================================
handle_ping(Host, Data) ->
PortNumber = decode_udp_message(Data),
contact_host(Host, PortNumber).
decode_udp_message(Data) when is_binary(Data) ->
% First 16 bits == Port number
<<PortNumber:16>> = Data,
PortNumber.
contact_host(Host, PortNumber) ->
tcp_client:connect(Host, PortNumber).
Result
I've changed my code based on your answers, decode_udp_message
is gone because handle_ping
does what I need.
handle_ping(Host, <<PortNumber:16>>) ->
contact_host(Host, PortNumber);
handle_ping(Host, Data) ->
%% Here I'll log the invalid datagrams but the process won't be restarted
I like the way it is now, by adding the following code I could handle protocol changes in the future without losing backwards compatibility with old servers:
handle_ping(Host, <<PortNumber:16, Foo:8, Bar:32>>) ->
contact_host(Host, PortNumber, Foo, Bar);
handle_ping(Host, <<PortNumber:16>>) ->
...
@Samuel-Rivas
tcp_client
is another gen_server with it's own supervisor, it will handle its own failures.
-> Socket = State
in now only present in the terminate
function. gen_udp:close(Socket).
is easier on the eyes.