3

I have a GenServerthat is connecting to a remote TCP connection via gen_tcp.

opts = [:binary, active: true, packet: :line] {:ok, socket} = :gen_tcp.connect('remote-url', 8000, opts}

I'm handling messages with:

def handle_info({:tcp, socket, msg}, stage) do IO.inspect msg {:noreply, state} end

Which works great. However, the TCP server is prone to timeouts. If I were using gen_tcp.recv, I could specify a timeout. However, I am using active: true to receive messages with handle_info, and not have to loop over and call recv. So, the GenServer happily waits for the next message, even if the server has timed out.

How can I have the GenServer trigger a function when it hasn't received a message from the TCP connection after X seconds? Am I stuck using recv?

zxq9
  • 13,020
  • 1
  • 43
  • 60
user2666425
  • 1,671
  • 1
  • 15
  • 21
  • Not sure why someone downvoted this. This is a basic puzzle many people face early on with Erlang, Elixir, LFE, etc. using `{active, true}` sockets -- and the solution is _not_ only applicable to Elixir. – zxq9 Jul 24 '16 at 02:31

1 Answers1

7

If the TCP connection itself has timed out then you should be receiving a closed socket message {tcp_closed, Socket}. Match on this in handle_info and that's it. As for establishing your own timeout on the connection, I usually use erlang:send_after/3 to send myself a message -- effectively adding a timeout message semantic that I can receive in handle_info or whatever receive service loop might exist.

erlang:send_after(?IDLE_TIMEOUT, self(), {tcp_timeout, Socket})

This must be paired with a cancellation of the timer every time traffic is received. This might look something like:

handle_info({tcp, Socket, Bin}, State = #s{timer = T, socket = Socket}) ->
    _ = erlang:cancel_timer(T),
    {Messages, NewState} = interpret(Bin, State),
    ok = handle(Messages),
    NewT = erlang:send_after(?IDLE_TIMEOUT, self(), {tcp_timeout, Socket})
    {noreply, NewState#s{timer = NewT}};
handle_info({tcp_closed, Socket}, State = #s{timer = T, socket = Socket}) ->
    _ = erlang:cancel_timer(T),
    NewState = handle_close(State),
    {noreply, NewState};
handle_info({tcp_timeout, Socket}, State = #s{socket = Socket}) ->
    NewState = handle_timeout(State),
    {noreply, NewState};
handle_info(Unexpected, State) ->
    ok = log(warning, "Received unexpected message: ~tp", [Unexpected]),
    {noreply, State}.
zxq9
  • 13,020
  • 1
  • 43
  • 60
  • Thanks for the answer! I also realized that Elixir `GenServer`s support returning a `timeout` value, so {:noreply, state, timeout}` that result in sending a timeout message if no other message is received within `timeout`. I'm not sure if that's present in Erlang `gen_servers` or if Elixir is adding it on, but this solved the problem for me. Your solution would work too if I didn't have access to that timeout helper. Thank you! – user2666425 Jul 25 '16 at 14:42
  • @user2666425 Yes, Erlang gen_servers have the exact same timeout value you can include in the return value from a `handle_*` call. *BUT* this is a timeout _for any message at all_, and that is not the same as a timeout _for a TCP message_. For example, if you spam your TCP handler with Erlang system messages it will be constantly resetting that timeout value and missing the fact that the socket has had no activity. That is why for this particular use I add my own `tcp_timeout` message as a message specific to socket traffic. – zxq9 Jul 26 '16 at 11:49
  • That makes perfect sense. Thank you very much for the clear and helpful answers! – user2666425 Jul 27 '16 at 20:11