I have a GenServer
that 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
?
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}.