-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,
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) ->
supervisor = Supervisor,
table_id = TableId
},
- {ok, State}.
+ State2 = reset_ping_timer(State),
+ {ok, State2}.
handle_call(_Request, _From, _State) ->
{reply, ok, _State}.
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) ->
% 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.", []),
_ -> 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.