From 3f44b2df50e30513ceca64319ff14366dff645d3 Mon Sep 17 00:00:00 2001 From: Matt Mullins Date: Sun, 23 Mar 2014 00:06:35 -0700 Subject: [PATCH] Add the ability to actively PING the server. 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 | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/irc/irc_conn.erl b/irc/irc_conn.erl index 17435ae..b09214a 100644 --- a/irc/irc_conn.erl +++ b/irc/irc_conn.erl @@ -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. -- 2.11.0