3
votes

I am learning Erlang. I want to make a UDP Listener that will be supervised by a supervisor. So if the listener process goes down supervisor will restart the process. Initially I just made a simple UDP listener which works as expected.

startudplistener() ->
    {ok, Socket} = gen_udp:open(9000,[binary,{active,false}]),
    Pid = spawn(pdmanager,udplistener,[Socket]),
    {ok, Pid}.

udplistener(Socket) ->
    {ok,Packet} = gen_udp:recv(Socket,0),
    spawn(pdmanager,handleudp,[Packet]),
    udplistener(Socket).

handleudp(Packet) ->
    {_,_, Msg} = Packet,
    io:format("I have got message : ~s ~n",[Msg]),
    {handeling, Packet}.

So, what I want to do is to monitor the udplistener process. For this first I modified my module to a gen_server one. Write a supervisor module afterwards. My supervisor looks like this:

-module(pdmanager_sup).
-behaviour(supervisor).

-export([start_link/1]).
-export([init/1]).

start_link(Port) ->
supervisor:start_link({local,?MODULE}, ?MODULE, Port).

init(Port) ->
    {ok, Socket} = gen_udp:open(Port, [binary, {active, false}]),
    {ok, {{one_for_one, 5, 60},
        [{listener,
            {pdmanager, start_link, [Socket]},
            permanent, 1000, worker, [pdmanager]}
        ]}}.

So what Im trying to so is to, open up a new udp socket and pass it to my server and the server will keep on listening to the socket while the supervisor will monitor it. So I came up with the following code.

start_link(Socket) ->
    gen_server:start_link({local, pdmanager}, pdmanager, Socket, []).

init(Socket) ->
    io:format("UDP Server starting ~n",[]),
    spawn_link(pdmanager,udplistener,[Socket]),
    {ok, #state{socket=Socket}}.

I am a bit confused with spawn_link that I added within my init function. spawn_link is opening up another process, however it is making a link with the calling process. As per my understanding my supervisor will monitor the calling process here. So, how would my supervisor behave incase my udplistener goes down? If it doesn't work the way I am expecting (im expecting it will restart my server) what is the best way to do it?

1

1 Answers

3
votes

Your supervisor init callback implementation that creates a socket and passes it to the worker is a problem.

Apart from the exceptional case of a release upgrade your supervisor init callback will actually only be called once ever, so creating the socket in the supervisor init and passing it to the worker means there is never any mechanism for the socket to be reopened. If however you open the socket in the init callback of your gen_server worker, any problems with the socket will get solved when the worker restarts.

The next problem is you are using the socket in inactive mode. Sockets in inactive mode are really only useful and good for a relatively bare process (i.e. not a gen_server say), because when you call gen_udp:recv it can block while waiting for data to arrive... and this means the gen_server is blocked and unable to service anything it should. So now you're going down the OTP path, with supervisors and gen_servers, you should switch to using the socket in active mode, which means the UDP packets will be sent to your gen_server as messages. You can then receive then by way of your handle_info callback implementation:

handle_info({udp, Socket, IP, InPortNo, Packet}, #state{socket=Socket}) ->
    io:format("whooopie, got a packet ~p~n", [Packet]),

If your gen_server worker dies, that's fine, the port will go too, and the supervisor will start a new worker which will just reopen the socket and continue receiving again...

Also in your workers init, after opening the socket, note that the socket is actually a port, you should probably link to it by calling erlang:link/1, like this:

{ok, Socket} = gen_udp:open(9000,[binary,{active,true}]),
erlang:link(Socket),

I'm not absolutely sure if gen_udp will do this anyway for you, but I can't see that it says so in the docs, and better safe than sorry. This now means if the port dies but not your worker, the link will cause your worker to also die, and the supervisor will restart your worker, which will restart your port. If you want to avoid your worker dying where you can you can trap exits instead, but simply allowing your worker to die fits better with Erlangs fail early principal IMO, and it means if your socket open is failing constantly, the supervisor will hit its restart intensity and do something different instead of letting your worker mindlessly going on forever trying to reopen the socket. So do that for now and change your strategy later if you have cause.