Current section

Files

Jump to
eredis src eredis_sentinel.erl
Raw

src/eredis_sentinel.erl

%% @private
-module(eredis_sentinel).
-behaviour(gen_server).
-include("eredis.hrl").
-include_lib("kernel/include/logger.hrl").
%% API
-export([start_link/2, stop/1, get_master/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-record(errors, {
sentinel_unreachable = 0 :: integer(),
master_unknown = 0 :: integer(),
master_unreachable = 0 :: integer(),
total = 0 :: integer()
}).
-record(eredis_sentinel_state, {
master_group :: atom(),
endpoints :: [{string() | {local, string()} , integer()}],
username :: obfuscated() | undefined,
password :: obfuscated() | undefined,
connect_timeout :: integer() | undefined,
socket_options :: list(),
tls_options :: list(),
conn_pid :: undefined | pid(),
errors :: #errors{}
}).
-define(CONNECT_TIMEOUT, 5000).
%% Sentinel errors
-define(SENTINEL_UNREACHABLE, sentinel_unreachable).
-define(MASTER_UNKNOWN, master_unknown).
-define(MASTER_UNREACHABLE, master_unreachable).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(atom(), list()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(Name, Options) ->
gen_server:start_link({local, Name}, ?MODULE, Options, []).
stop(Pid) ->
gen_server:call(Pid, stop).
get_master(Pid) ->
gen_server:call(Pid, get_master).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #eredis_sentinel_state{}} | {ok, State :: #eredis_sentinel_state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init(Options) ->
process_flag(trap_exit, true),
MasterGroup = proplists:get_value(master_group, Options, mymaster),
Endpoints = proplists:get_value(endpoints, Options, [{"127.0.0.1", 26379}]),
Username = proplists:get_value(username, Options, undefined),
Password = proplists:get_value(password, Options, undefined),
ConnectTimeout = proplists:get_value(connect_timeout, Options, ?CONNECT_TIMEOUT),
SocketOptions = proplists:get_value(socket_options, Options, []),
TlsOptions = proplists:get_value(tls, Options, []),
{ok, #eredis_sentinel_state{master_group = MasterGroup,
endpoints = Endpoints,
username = obfuscate(Username),
password = obfuscate(Password),
connect_timeout = ConnectTimeout,
socket_options = SocketOptions,
tls_options = TlsOptions,
conn_pid = undefined,
errors = #errors{}}}.
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #eredis_sentinel_state{}) ->
{reply, Reply :: term(), NewState :: #eredis_sentinel_state{}} |
{reply, Reply :: term(), NewState :: #eredis_sentinel_state{}, timeout() | hibernate} |
{noreply, NewState :: #eredis_sentinel_state{}} |
{noreply, NewState :: #eredis_sentinel_state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #eredis_sentinel_state{}} |
{stop, Reason :: term(), NewState :: #eredis_sentinel_state{}}).
handle_call(get_master, _From, State) ->
case query_master(State#eredis_sentinel_state{errors = #errors{}}) of
{ok, {Host, Port}, S1} ->
{reply, {ok, Host, Port}, S1};
{error, Error, S1} ->
{reply, {error, Error}, S1}
end;
handle_call(stop, _From, State) ->
{stop, normal, ok, State}.
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #eredis_sentinel_state{}) ->
{noreply, NewState :: #eredis_sentinel_state{}} |
{noreply, NewState :: #eredis_sentinel_state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #eredis_sentinel_state{}}).
handle_cast(_Request, State = #eredis_sentinel_state{}) ->
{noreply, State}.
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #eredis_sentinel_state{}) ->
{noreply, NewState :: #eredis_sentinel_state{}} |
{noreply, NewState :: #eredis_sentinel_state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #eredis_sentinel_state{}}).
%% Current sentinel connection broken
handle_info({'EXIT', Pid, _Reason}, #eredis_sentinel_state{conn_pid = Pid} = S) ->
{noreply, S#eredis_sentinel_state{conn_pid = undefined}};
handle_info({'EXIT', _Pid, _Reason}, S) ->
{stop, normal, S};
handle_info(_Info, State) ->
{noreply, State}.
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #eredis_sentinel_state{}) -> term()).
terminate(_Reason, _State = #eredis_sentinel_state{conn_pid = undefined}) ->
ok;
terminate(_Reason, _State = #eredis_sentinel_state{conn_pid = Pid}) ->
eredis:stop(Pid),
ok.
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #eredis_sentinel_state{},
Extra :: term()) ->
{ok, NewState :: #eredis_sentinel_state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #eredis_sentinel_state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
rotate([X|Xs]) -> Xs ++ [X].
%% Finding new master host for named cluster:
%% * First try to query already connected sentinel if we have one.
%% * If this failed try to connect and query all sentinels starting from the last connected one.
%% * If connected sentinel returns port:ip - return {ok, {Host, Port}} and remember connection pid.
%% * In case no sentinels return valid answer - response with error:
%% * If all sentinels failed connect to - return {error, sentinel_unreachable}
%% * If all connected sentinels return null - return {error, sentinel_master_unknown}
%% * If some of connected sentinels return -IDONTKNOW - return {error, sentinel_master_unreachable}
-spec query_master(#eredis_sentinel_state{}) ->
{ok, {string(), integer()}, #eredis_sentinel_state{}} | {error, any(), #eredis_sentinel_state{}}.
%% All sentinels return errors
query_master(#eredis_sentinel_state{errors = Errors, endpoints = Sentinels} = S)
when Errors#errors.total >= length(Sentinels) ->
#errors{sentinel_unreachable=SU, master_unknown=MUK, master_unreachable=MUR} = Errors,
if
SU == length(Sentinels) ->
{error, ?SENTINEL_UNREACHABLE, S};
MUK > 0, MUR == 0 ->
{error, ?MASTER_UNKNOWN, S};
true ->
{error, ?MASTER_UNREACHABLE, S}
end;
%% No connected sentinel
query_master(#eredis_sentinel_state{conn_pid=undefined,
endpoints = [{H, P} | _],
username = Username,
password = Password,
connect_timeout = ConnectTimeout,
socket_options = SocketOptions,
tls_options = TlsOptions} = S
) ->
case eredis:start_link([{host, H},
{port, P},
{username, Username},
{password, Password},
{connect_timeout, ConnectTimeout},
{socket_options, SocketOptions},
{tls, TlsOptions},
{reconnect_sleep, no_reconnect}]) of
{ok, ConnPid} ->
query_master(S#eredis_sentinel_state{conn_pid=ConnPid});
{error, E} ->
?LOG_WARNING("Error connecting to sentinel at ~p:~p : ~p", [H, P, E]),
Errors = update_errors(?SENTINEL_UNREACHABLE, S#eredis_sentinel_state.errors),
Sentinels = rotate(S#eredis_sentinel_state.endpoints),
query_master(S#eredis_sentinel_state{endpoints = Sentinels, errors = Errors})
end;
%% Sentinel connected
query_master(#eredis_sentinel_state{conn_pid=ConnPid,
master_group = MasterGroup,
endpoints=[{H, P}|_]} = S) when is_pid(ConnPid)->
case query_master(ConnPid, MasterGroup) of
{ok, HostPort} ->
{ok, HostPort, S};
{error, Error} ->
?LOG_WARNING("Master request for ~p to sentinel ~p:~p failed with ~p",
[MasterGroup, H, P, Error]),
eredis:stop(ConnPid),
Errors = update_errors(Error, S#eredis_sentinel_state.errors),
Sentinels = rotate(S#eredis_sentinel_state.endpoints),
query_master(S#eredis_sentinel_state{conn_pid = undefined, errors = Errors, endpoints = Sentinels})
end.
update_errors(E, #errors{sentinel_unreachable=SU, master_unknown=MUK, master_unreachable=MUR, total = T} = Errors) ->
Errors1 =
case E of
?SENTINEL_UNREACHABLE ->
Errors#errors{sentinel_unreachable = SU + 1};
?MASTER_UNKNOWN ->
Errors#errors{master_unknown = MUK + 1};
?MASTER_UNREACHABLE ->
Errors#errors{master_unreachable = MUR + 1}
end,
Errors1#errors{total = T + 1}.
query_master(Pid, MasterGroup) ->
Req = ["SENTINEL", "get-master-addr-by-name", atom_to_list(MasterGroup)],
try get_master_response(eredis:q(Pid, Req)) of
Result ->
Result
catch Type:Error ->
?LOG_ERROR("Sentinel error getting master ~p: ~p:~p",
[MasterGroup, Type, Error]),
{error, Error}
end.
get_master_response({ok, [HostBin, PortBin]}) ->
Host = binary_to_list(HostBin),
Port = list_to_integer(binary_to_list(PortBin)),
{ok, {Host, Port}};
get_master_response({ok, undefined}) ->
{error, ?MASTER_UNKNOWN};
get_master_response({error, <<"IDONTKNOW", _Rest/binary >>}) ->
{error, ?MASTER_UNREACHABLE}.
%% Obfuscate a string by wrapping it in a fun that returns the string when
%% applied. This hides the secrets from stacktraces and logs.
-spec obfuscate(iodata() | obfuscated() | undefined) ->
obfuscated() | undefined.
obfuscate(undefined) ->
undefined;
obfuscate(String) when is_list(String); is_binary(String) ->
fun () -> String end;
obfuscate(Fun) when is_function(Fun, 0) ->
%% Already obfuscated
Fun.