Add the ability to actively PING the server.
authorMatt Mullins <mmullins@mmlx.us>
Sun, 23 Mar 2014 07:06:35 +0000 (00:06 -0700)
committerMatt Mullins <mmullins@mmlx.us>
Sun, 23 Mar 2014 07:08:19 +0000 (00:08 -0700)
This currently forcefully kills the irc_conn process once a ping timeout
occurs, without any nice QUIT message or anything.

Bump the state version because I added another variable to irc_state.

irc/irc_conn.erl

index 17435ae..b09214a 100644 (file)
@@ -1,8 +1,10 @@
 -module(irc_conn).
 -behavior(gen_server).
--vsn(4).
+-vsn(5).
 
 -define(RECONNECT_TIME, 30000).
+-define(PING_PERIOD, timer:seconds(30)).
+-define(PING_TIMEOUT, timer:seconds(5)).
 
 -export([
           start_link/3,
@@ -28,7 +30,8 @@
                     buffer = "",   % for the TCP session
                     supervisor,    % irc_net_sup instance
                     table_id,      % ets table to hold shared data
-                    object_sup = none % supervisor for irc_object_*
+                    object_sup = none, % supervisor for irc_object_*
+                    ping_timer = none  % timer ID for next "ping the server" event.
                    }).
 
 start_link(Instance, Supervisor, TableId) ->
@@ -80,7 +83,8 @@ init({Instance, Supervisor, TableId}) ->
                        supervisor = Supervisor,
                        table_id = TableId
                       },
-    {ok, State}.
+    State2 = reset_ping_timer(State),
+    {ok, State2}.
 
 handle_call(_Request, _From, _State) ->
     {reply, ok, _State}.
@@ -119,7 +123,16 @@ handle_info({tcp, _Socket, Data}, State = #irc_state{ socket = _Socket }) ->
         none ->
             State#irc_state{buffer = Buf1}
         end,
-    {noreply, NewState}.
+    {noreply, NewState};
+handle_info(send_ping_command, #irc_state{ping_timer = Timer} = State) ->
+    error_logger:info_msg("Sending a PING to the server."),
+    % Ensure that we only ever have one timer at a time generating ping_timeout.
+    timer:cancel(Timer),
+    send_command(#irc_command{command = "PING", trailing = "foo"}),
+    {ok, NewTimer} = timer:send_after(?PING_TIMEOUT, ping_timeout),
+    {noreply, State#irc_state{ping_timer = NewTimer}};
+handle_info(ping_timeout, State) ->
+    {stop, ping_timeout, State}.
 
 %% @doc Handles a line received from the IRC server.
 do_line(Line, State) ->
@@ -131,7 +144,10 @@ do_line(Line, State) ->
             % leaving the rest of the line (usually a timestamp) alone
             NewCommand = Command#irc_command{prefix = none, command = "PONG"},
             send_command(NewCommand),
-            State;
+            reset_ping_timer(State);
+        "PONG" ->
+            error_logger:info_msg("Received a PONG from the server"),
+            reset_ping_timer(State);
         "376" when State#irc_state.joined == false ->
             % end of MOTD, which should normally be sent on connect
             error_logger:info_msg("Joining my channels now.", []),
@@ -155,6 +171,34 @@ do_privmsg(_Command = #irc_command{middles = Middles, trailing = Text}) ->
         _ -> ok
     end.
 
+%% @doc Reset the state machine for actively pinging the server.
+%%
+%% This initially sets a timer that sends a send_ping_command message.  That
+%% message causes a timer that will send a ping_timeout message, which
+%% immediately tears down the connection.
+%%
+%% The process of canceling the timers is inherently racy, since it's entirely
+%% possible for the timer to fire before we get around to canceling it.  My
+%% analysis:
+%%
+%% If a send_ping_command message is sent while this function is running, then:
+%%   a) We'll send an extra PING to the server, but that's OK, and
+%%   b) The timer set by this function will immediately be canceled by the
+%%      handle_info(..., send_ping_command).  But even if it weren't, see (a).
+%%
+%% If a ping_timeout message is sent while either this function or
+%% handle_info(..., send_ping_command) is running, then we will proceed to tear
+%% down the connection.  This means one of:
+%%   a) there was a PING which the server took longer than ?PING_TIMEOUT to
+%%      respond to, or
+%%   b) we spent longer than ?PING_TIMEOUT servicing a message
+%% Both of these are bad, so let's just kill it anyway.
+reset_ping_timer(#irc_state{ping_timer = Timer} = State) ->
+    error_logger:info_msg("Resetting the ping-timer state."),
+    timer:cancel(Timer),
+    {ok, NewTimer} = timer:send_after(?PING_PERIOD, send_ping_command),
+    State#irc_state{ping_timer = NewTimer}.
+
 terminate(_Reason, _State) ->
     ok.