erlangelixirgen-tcp

How can I detect a TCP timeout on an active socket in Elixir with gen_tcp?


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?


Solution

  • 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}.