Packages
ra
1.0.1
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.2
3.0.1
3.0.0
3.0.0-beta.1
2.17.3
2.17.2
2.17.1
2.17.0
2.16.13
2.16.12
2.16.11
2.16.10
2.16.9
2.16.8
2.16.7
2.16.6
2.16.5
2.16.4
2.16.3
2.16.2
2.16.1
2.16.0
2.16.0-pre.12
2.16.0-pre.11
2.16.0-pre.10
2.16.0-pre.9
2.16.0-pre.8
2.16.0-pre.7
2.16.0-pre.6
2.16.0-pre.5
2.16.0-pre.4
2.16.0-pre.3
2.16.0-pre.2
2.16.0-pre.1
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.0
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.13.0-pre.1
2.12.0
2.11.0
2.11.0-pre.1
2.10.2-pre.2
2.10.2-pre.1
2.10.1
2.10.0
2.10.0-pre.3
2.10.0-pre.2
2.10.0-pre.1
2.9.10-pre.1
2.9.1
2.9.1-pre.2
2.9.1-pre.1
2.9.0
2.8.0
retired
2.7.3
2.7.2
2.7.1
2.7.0
2.7.0-pre.3
2.7.0-pre.2
2.7.0-pre.1
2.6.3
2.6.2
2.6.1
2.6.0-pre.1
2.5.1
2.5.1-pre.1
2.5.0
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
retired
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.9.6
0.9.5
0.9.4
0.9.2
0.3.3
retired
0.3.2
retired
0.3.1
retired
Raft library
Current section
Files
Jump to
Current section
Files
src/ra_server_proc.erl
%% @hidden
-module(ra_server_proc).
-behaviour(gen_statem).
-include("ra.hrl").
%% State functions
-export([
recover/3,
recovered/3,
leader/3,
pre_vote/3,
candidate/3,
follower/3,
receive_snapshot/3,
await_condition/3,
terminating_leader/3,
terminating_follower/3
]).
%% gen_statem callbacks
-export([
init/1,
format_status/2,
handle_event/4,
terminate/3,
code_change/4,
callback_mode/0
]).
%% API
-export([start_link/1,
command/3,
cast_command/2,
cast_command/3,
query/4,
state_query/3,
trigger_election/2,
ping/2,
log_fold/4,
transfer_leadership/3
]).
-export([send_rpc/2]).
-define(SERVER, ?MODULE).
-define(DEFAULT_BROADCAST_TIME, 100).
-define(DEFAULT_ELECTION_MULT, 5).
-define(TICK_INTERVAL_MS, 1000).
-define(DEFAULT_STOP_FOLLOWER_ELECTION, false).
-define(DEFAULT_AWAIT_CONDITION_TIMEOUT, 30000).
%% Utilisation average calculations are all in μs.
-define(USE_AVG_HALF_LIFE, 1000000.0).
-define(INSTALL_SNAP_RPC_TIMEOUT, 120 * 1000).
-define(DEFAULT_RECEIVE_SNAPSHOT_TIMEOUT, 30000).
-define(DEFAULT_SNAPSHOT_CHUNK_SIZE, 1000000). % 1MB
-define(FLUSH_COMMANDS_SIZE, 25).
-define(HANDLE_EFFECTS(Effects, EvtType, State0),
handle_effects(?FUNCTION_NAME, Effects, EvtType, State0)).
-type query_fun() :: ra:query_fun().
-type ra_command() :: {ra_server:command_type(), term(),
ra_server:command_reply_mode()}.
-type ra_command_priority() :: normal | low.
-type ra_leader_call_ret(Result) :: {ok, Result, Leader::ra_server_id()} |
{error, term()} |
{timeout, ra_server_id()}.
-type ra_cmd_ret() :: ra_leader_call_ret(term()).
-type gen_statem_start_ret() :: {ok, pid()} | ignore | {error, term()}.
-type safe_call_ret(T) :: timeout | {error, noproc | nodedown} | T.
-type states() :: leader | follower | candidate | await_condition.
%% ra_event types
-type ra_event_reject_detail() :: {not_leader, Leader :: maybe(ra_server_id()),
ra_server:command_correlation()}.
-type ra_event_body() ::
% used for notifying senders of the ultimate fate of their command
% sent using ra:pipeline_command/3|4
{applied, [{ra_server:command_correlation(), Reply :: term()}]} |
{rejected, ra_event_reject_detail()} |
% used to send message side-effects emitted by the state machine
{machine, term()}.
-type ra_event() :: {ra_event, Sender :: ra_server_id(), ra_event_body()}.
%% the Sender is the ra process that emitted the ra_event.
-type server_loc() :: ra_server_id() | [ra_server_id()].
-export_type([ra_leader_call_ret/1,
ra_cmd_ret/0,
safe_call_ret/1,
ra_event_reject_detail/0,
ra_event/0,
ra_event_body/0]).
-type monitors() ::
#{pid() | node() => {machine | snapshot_sender, maybe(reference())}}.
%% the ra server proc keeps monitors on behalf of different components
%% the state machine, log and server code. The tag is used to determine
%% which component to dispatch the down to
-record(conf, {log_id :: unicode:chardata(),
name :: atom(),
broadcast_time = ?DEFAULT_BROADCAST_TIME :: non_neg_integer(),
tick_timeout :: non_neg_integer(),
await_condition_timeout :: non_neg_integer(),
ra_event_formatter :: undefined | {module(), atom(), [term()]},
flush_commands_size = ?FLUSH_COMMANDS_SIZE :: non_neg_integer()
}).
-record(state, {conf :: #conf{},
server_state :: ra_server:ra_server_state(),
monitors = #{} :: monitors(),
pending_commands = [] :: [{{pid(), any()}, term()}],
leader_monitor :: reference() | undefined,
delayed_commands =
queue:new() :: queue:queue(ra_server:command())
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link(ra_server:ra_server_config()) -> gen_statem_start_ret().
start_link(Config = #{id := Id}) ->
Name = ra_lib:ra_server_id_to_local_name(Id),
gen_statem:start_link({local, Name}, ?MODULE, Config, []).
-spec command(server_loc(), ra_command(), timeout()) ->
ra_cmd_ret().
command(ServerLoc, Cmd, Timeout) ->
leader_call(ServerLoc, {command, normal, Cmd}, Timeout).
-spec cast_command(ra_server_id(), ra_command()) -> ok.
cast_command(ServerId, Cmd) ->
gen_statem:cast(ServerId, {command, low, Cmd}).
-spec cast_command(ra_server_id(), ra_command_priority(), ra_command()) -> ok.
cast_command(ServerId, Priority, Cmd) ->
gen_statem:cast(ServerId, {command, Priority, Cmd}).
-spec query(server_loc(), query_fun(),
local | consistent | leader, timeout()) ->
ra_server_proc:ra_leader_call_ret(term())
| {ok, Reply :: term(), ra_server_id() | not_known}.
query(ServerLoc, QueryFun, local, Timeout) ->
statem_call(ServerLoc, {local_query, QueryFun}, Timeout);
query(ServerLoc, QueryFun, leader, Timeout) ->
leader_call(ServerLoc, {local_query, QueryFun}, Timeout);
query(ServerLoc, QueryFun, consistent, Timeout) ->
leader_call(ServerLoc, {consistent_query, QueryFun}, Timeout).
-spec log_fold(ra_server_id(), fun(), term(), integer()) -> term().
log_fold(ServerId, Fun, InitialState, Timeout) ->
gen_statem:call(ServerId, {log_fold, Fun, InitialState}, Timeout).
%% used to query the raft state rather than the machine state
-spec state_query(server_loc(), all | members | machine, timeout()) ->
ra_leader_call_ret(term()).
state_query(ServerLoc, Spec, Timeout) ->
leader_call(ServerLoc, {state_query, Spec}, Timeout).
-spec trigger_election(ra_server_id(), timeout()) -> ok.
trigger_election(ServerId, Timeout) ->
gen_statem:call(ServerId, trigger_election, Timeout).
-spec transfer_leadership(ra_server_id(), ra_server_id(), timeout()) ->
ok | already_leader.
transfer_leadership(ServerId, TargetServerId, Timeout) ->
leader_call(ServerId, {transfer_leadership, TargetServerId}, Timeout).
-spec ping(ra_server_id(), timeout()) -> safe_call_ret({pong, states()}).
ping(ServerId, Timeout) ->
gen_statem_safe_call(ServerId, ping, Timeout).
leader_call(ServerLoc, Msg, Timeout) ->
statem_call(ServerLoc, {leader_call, Msg}, Timeout).
statem_call(ServerIds, Msg, Timeout)
when is_list(ServerIds) ->
multi_statem_call(ServerIds, Msg, [], Timeout);
statem_call(ServerId, Msg, Timeout) ->
case gen_statem_safe_call(ServerId, Msg, Timeout) of
{redirect, Leader} ->
statem_call(Leader, Msg, Timeout);
{wrap_reply, Reply} ->
{ok, Reply, ServerId};
{error, _} = E ->
E;
timeout ->
{timeout, ServerId};
Reply ->
Reply
end.
multi_statem_call([ServerId | ServerIds], Msg, Errs, Timeout) ->
case statem_call(ServerId, Msg, Timeout) of
{Tag, _} = E
when Tag == error orelse Tag == timeout ->
case ServerIds of
[] ->
{error, {no_more_servers_to_try, [E | Errs]}};
_ ->
multi_statem_call(ServerIds, Msg, [E | Errs], Timeout)
end;
Reply ->
Reply
end.
%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================
init(Config0 = #{id := Id, cluster_name := ClusterName}) ->
process_flag(trap_exit, true),
Config = maps:merge(config_defaults(), Config0),
#{id := {_, UId, LogId},
cluster := Cluster} = ServerState = ra_server:init(Config),
Key = ra_lib:ra_server_id_to_local_name(Id),
% ensure ra_directory has the new pid
yes = ra_directory:register_name(UId, self(),
maps:get(parent, Config, undefined), Key,
ClusterName),
% ensure each relevant erlang node is connected
Peers = maps:keys(maps:remove(Id, Cluster)),
%% as most messages are sent using noconnect we explicitly attempt to
%% connect to all relevant nodes
_ = spawn(fun () ->
_ = lists:foreach(fun ({_, Node}) ->
net_kernel:connect_node(Node);
(_) -> node()
end, Peers)
end),
TickTime = maps:get(tick_timeout, Config),
AwaitCondTimeout = maps:get(await_condition_timeout, Config),
RaEventFormatterMFA = maps:get(ra_event_formatter, Config, undefined),
FlushCommandsSize = maps:get(low_priority_commands_flush_size, Config,
?FLUSH_COMMANDS_SIZE),
State = #state{conf = #conf{log_id = LogId,
name = Key,
tick_timeout = TickTime,
await_condition_timeout = AwaitCondTimeout,
ra_event_formatter = RaEventFormatterMFA,
flush_commands_size = FlushCommandsSize},
server_state = ServerState},
ok = net_kernel:monitor_nodes(true),
{ok, recover, State, [{next_event, cast, go}]}.
%% callback mode
callback_mode() -> [state_functions, state_enter].
%%%===================================================================
%%% State functions
%%%===================================================================
recover(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
recover(_EventType, go, State = #state{server_state = ServerState0}) ->
ServerState = ra_server:recover(ServerState0),
true = erlang:garbage_collect(),
%% we have to issue the next_event here so that the recovered state is
%% only passed through very briefly
{next_state, recovered, State#state{server_state = ServerState},
[{next_event, internal, next}]};
recover(_, _, State) ->
% all other events need to be postponed until we can return
% `next_event` from init
{keep_state, State, {postpone, true}}.
%% this is a passthrough state to allow state machines to emit node local
%% effects post recovery
recovered(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
recovered(internal, next, #state{server_state = ServerState} = State) ->
% New cluster starts should be coordinated and elections triggered
% explicitly hence if this is a new one we wait here.
% Else we set an election timer
Actions = case ra_server:is_new(ServerState) of
true ->
[];
false ->
?DEBUG("~s: is not new, setting election timeout.~n",
[log_id(State)]),
[election_timeout_action(short, State)]
end,
_ = ets:insert(ra_metrics, ra_server:metrics(ServerState)),
{next_state, follower, State, set_tick_timer(State, Actions)}.
leader(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
leader(EventType, {leader_call, Msg}, State) ->
% no need to redirect
leader(EventType, Msg, State);
leader(EventType, {leader_cast, Msg}, State) ->
leader(EventType, Msg, State);
leader(EventType, {command, normal, {CmdType, Data, ReplyMode}},
#state{server_state = ServerState0} = State0) ->
%% normal priority commands are written immediately
Cmd = make_command(CmdType, EventType, Data, ReplyMode),
{leader, ServerState, Effects} =
ra_server:handle_leader({command, Cmd}, ServerState0),
{State, Actions} =
?HANDLE_EFFECTS(Effects, EventType,
State0#state{server_state = ServerState}),
{keep_state, State, Actions};
leader(EventType, {command, low, {CmdType, Data, ReplyMode}},
#state{delayed_commands = Delayed} = State0) ->
%% cache the low priority command until the flush_commands message arrives
Cmd = make_command(CmdType, EventType, Data, ReplyMode),
%% if there are no prior delayed commands
%% (and thus no action queued to do so)
%% queue a state timeout to flush them
%% We use a cast to ourselves instead of a zero timeout as we want to
%% get onto the back of the erlang mailbox not just the current gen_statem
%% event buffer.
case queue:is_empty(Delayed) of
true ->
ok = gen_statem:cast(self(), flush_commands);
false ->
ok
end,
{keep_state, State0#state{delayed_commands = queue:in(Cmd, Delayed)}, []};
leader(EventType, flush_commands,
#state{conf = #conf{flush_commands_size = Size},
server_state = ServerState0,
delayed_commands = Delayed0} = State0) ->
{DelQ, Delayed} = queue_take(Size, Delayed0),
%% write a batch of delayed commands
{leader, ServerState, Effects} =
ra_server:handle_leader({commands, Delayed}, ServerState0),
{State, Actions} =
?HANDLE_EFFECTS(Effects, EventType,
State0#state{server_state = ServerState}),
case queue:is_empty(DelQ) of
true ->
ok;
false ->
ok = gen_statem:cast(self(), flush_commands)
end,
{keep_state, State#state{delayed_commands = DelQ}, Actions};
leader({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Reply = perform_local_query(QueryFun, id(State), ServerState),
{keep_state, State, [{reply, From, Reply}]};
leader({call, From}, {state_query, Spec},
#state{server_state = ServerState} = State) ->
Reply = {ok, do_state_query(Spec, ServerState), id(State)},
{keep_state, State, [{reply, From, Reply}]};
leader({call, From}, {consistent_query, QueryFun},
#state{server_state = ServerState0} = State0) ->
{leader, ServerState1, Effects} =
ra_server:handle_leader({consistent_query, From, QueryFun},
ServerState0),
{State1, Actions} =
?HANDLE_EFFECTS(Effects, {call, From},
State0#state{server_state = ServerState1}),
{keep_state, State1, Actions};
leader({call, From}, ping, State) ->
{keep_state, State, [{reply, From, {pong, leader}}]};
leader(info, {node_event, _Node, _Evt}, State) ->
{keep_state, State, []};
leader(info, {'DOWN', MRef, process, Pid, Info},
#state{monitors = Monitors0,
server_state = ServerState0} = State0) ->
case maps:take(Pid, Monitors0) of
{{Comp, MRef}, Monitors} ->
{_, ServerState, Effects} =
ra_server:handle_down(?FUNCTION_NAME, Comp, Pid, Info,
ServerState0),
{State, Actions} =
?HANDLE_EFFECTS(Effects, cast,
State0#state{server_state = ServerState,
monitors = Monitors}),
{keep_state, State, Actions};
error ->
{keep_state, State0, []}
end;
leader(info, {NodeEvt, Node},
#state{monitors = Monitors0,
server_state = ServerState0} = State0)
when NodeEvt =:= nodedown orelse NodeEvt =:= nodeup ->
case Monitors0 of
#{Node := _} ->
% there is a monitor for the node
Cmd = make_command('$usr', cast,
{NodeEvt, Node}, noreply),
{leader, ServerState, Effects} =
ra_server:handle_leader({command, Cmd}, ServerState0),
{State, Actions} =
?HANDLE_EFFECTS(Effects, cast,
State0#state{server_state = ServerState}),
{keep_state, State, Actions};
_ ->
{keep_state, State0, []}
end;
leader(_, tick_timeout, State0) ->
{State1, RpcEffs} = make_rpcs(State0),
ServerState = State1#state.server_state,
Effects = ra_server:tick(ServerState),
{State, Actions} = ?HANDLE_EFFECTS(RpcEffs ++ Effects, cast, State1),
_ = ets:insert(ra_metrics, ra_server:metrics(ServerState)),
true = erlang:garbage_collect(),
{keep_state, State, set_tick_timer(State, Actions)};
leader({timeout, Name}, machine_timeout,
#state{server_state = ServerState0} = State0) ->
% the machine timer timed out, add a timeout message
Cmd = make_command('$usr', cast, {timeout, Name}, noreply),
{leader, ServerState, Effects} = ra_server:handle_leader({command, Cmd},
ServerState0),
{State, Actions} = ?HANDLE_EFFECTS(Effects, cast,
State0#state{server_state =
ServerState}),
{keep_state, State, Actions};
leader({call, From}, trigger_election, State) ->
{keep_state, State, [{reply, From, ok}]};
leader({call, From}, {log_fold, Fun, Term}, State) ->
fold_log(From, Fun, Term, State);
leader(EventType, Msg, State0) ->
case handle_leader(Msg, State0) of
{leader, State1, Effects1} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects1, EventType, State1),
{keep_state, State, Actions};
{follower, State1, Effects1} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects1,
EventType,
State1),
% demonitor when stepping down
ok = lists:foreach(fun ({_, Ref}) when is_reference(Ref) ->
erlang:demonitor(Ref);
(_) ->
%% the monitor is a node
%% all nodes are always monitored
ok
end, maps:values(State#state.monitors)),
{next_state, follower, State#state{monitors = #{}},
maybe_set_election_timeout(State, Actions)};
{stop, State1, Effects} ->
% interact before shutting down in case followers need
% to know about the new commit index
{State, _Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{stop, normal, State};
{delete_and_terminate, State1, Effects} ->
{State2, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = send_rpcs(State2),
case ra_server:is_fully_replicated(State#state.server_state) of
true ->
{stop, {shutdown, delete}, State};
false ->
{next_state, terminating_leader, State, Actions}
end;
{await_condition, State1, Effects1} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects1, EventType, State1),
{next_state, await_condition, State, Actions}
end.
candidate(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
candidate({call, From}, {leader_call, Msg},
#state{pending_commands = Pending} = State) ->
{keep_state, State#state{pending_commands = [{From, Msg} | Pending]}};
candidate(cast, {command, _Priority,
{_CmdType, _Data, {notify, Corr, Pid}}},
State) ->
ok = reject_command(Pid, Corr, State),
{keep_state, State, []};
candidate({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Reply = perform_local_query(QueryFun, not_known, ServerState),
{keep_state, State, [{reply, From, Reply}]};
candidate({call, From}, ping, State) ->
{keep_state, State, [{reply, From, {pong, candidate}}]};
candidate(info, {node_event, _Node, _Evt}, State) ->
{keep_state, State};
candidate(_, tick_timeout, State0) ->
State = maybe_persist_last_applied(State0),
_ = ets:insert(ra_metrics, ra_server:metrics(State#state.server_state)),
{keep_state, State, set_tick_timer(State, [])};
candidate({call, From}, trigger_election, State) ->
{keep_state, State, [{reply, From, ok}]};
candidate(EventType, Msg, #state{pending_commands = Pending} = State0) ->
case handle_candidate(Msg, State0) of
{candidate, State1, Effects} ->
{State, Actions0} = ?HANDLE_EFFECTS(Effects, EventType, State1),
Actions = maybe_add_election_timeout(Msg, Actions0, State),
{keep_state, State, Actions};
{follower, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, follower, State,
% set an election timeout here to ensure an unelectable
% node doesn't cause an electable one not to trigger
% another election when not using follower timeouts
maybe_set_election_timeout(State, Actions)};
{leader, State1, Effects} ->
{State2, Actions0} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = State2#state{pending_commands = []},
%% reset the tick timer to avoid it triggering early after a leader
%% change
Actions = set_tick_timer(State2, Actions0),
% inject a bunch of command events to be processed when node
% becomes leader
NextEvents = [{next_event, {call, F}, Cmd} || {F, Cmd} <- Pending],
{next_state, leader, State, Actions ++ NextEvents}
end.
pre_vote(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
pre_vote({call, From}, {leader_call, Msg},
State = #state{pending_commands = Pending}) ->
{keep_state, State#state{pending_commands = [{From, Msg} | Pending]}};
pre_vote(cast, {command, _Priority,
{_CmdType, _Data, {notify, Corr, Pid}}},
State) ->
ok = reject_command(Pid, Corr, State),
{keep_state, State, []};
pre_vote({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Reply = perform_local_query(QueryFun, not_known, ServerState),
{keep_state, State, [{reply, From, Reply}]};
pre_vote({call, From}, ping, State) ->
{keep_state, State, [{reply, From, {pong, pre_vote}}]};
pre_vote(info, {node_event, _Node, _Evt}, State) ->
{keep_state, State};
pre_vote(_, tick_timeout, State0) ->
State = maybe_persist_last_applied(State0),
_ = ets:insert(ra_metrics, ra_server:metrics(State#state.server_state)),
{keep_state, State, set_tick_timer(State, [])};
pre_vote({call, From}, trigger_election, State) ->
{keep_state, State, [{reply, From, ok}]};
pre_vote(EventType, Msg, State0) ->
case handle_pre_vote(Msg, State0) of
{pre_vote, State1, Effects} ->
{State, Actions0} = ?HANDLE_EFFECTS(Effects, EventType, State1),
Actions = maybe_add_election_timeout(Msg, Actions0, State),
{keep_state, State, Actions};
{follower, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, follower, State,
% always set an election timeout here to ensure an unelectable
% node doesn't cause an electable one not to trigger
% another election when not using follower timeouts
[election_timeout_action(long, State) | Actions]};
{candidate, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, candidate, State,
[election_timeout_action(long, State) | Actions]}
end.
follower(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
follower({call, From}, {leader_call, Msg}, State) ->
maybe_redirect(From, Msg, State);
follower(_, {command, Priority, {_CmdType, Data, noreply}},
State) ->
% forward to leader
case leader_id(State) of
undefined ->
?WARN("~s: leader cast - leader not known. "
"Command is dropped.~n", [log_id(State)]),
{keep_state, State, []};
LeaderId ->
?INFO("~s: follower leader cast - redirecting to ~w ~n",
[log_id(State), LeaderId]),
ok = ra:pipeline_command(LeaderId, Data, no_correlation, Priority),
{keep_state, State, []}
end;
follower(cast, {command, _Priority,
{_CmdType, _Data, {notify, Corr, Pid}}},
State) ->
ok = reject_command(Pid, Corr, State),
{keep_state, State, []};
follower({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Leader = case ra_server:leader_id(ServerState) of
undefined -> not_known;
L -> L
end,
Reply = perform_local_query(QueryFun, Leader, ServerState),
{keep_state, State, [{reply, From, Reply}]};
follower({call, From}, trigger_election, State) ->
?DEBUG("~s: election triggered by ~w", [log_id(State), element(1, From)]),
{keep_state, State, [{reply, From, ok},
{next_event, cast, election_timeout}]};
follower({call, From}, ping, State) ->
{keep_state, State, [{reply, From, {pong, follower}}]};
follower(info, {'DOWN', MRef, process, _Pid, Info},
#state{leader_monitor = MRef} = State) ->
?INFO("~s: Leader monitor down with ~W, setting election timeout~n",
[log_id(State), Info, 8]),
case Info of
noconnection ->
{keep_state, State#state{leader_monitor = undefined},
[election_timeout_action(short, State)]};
_ ->
{keep_state, State#state{leader_monitor = undefined},
[election_timeout_action(really_short, State)]}
end;
follower(info, {'DOWN', MRef, process, Pid, Info},
#state{monitors = Monitors0, server_state = ServerState0} = State0) ->
case maps:take(Pid, Monitors0) of
{{Comp, MRef}, Monitors} ->
{_, ServerState, Effects} =
ra_server:handle_down(?FUNCTION_NAME, Comp, Pid, Info,
ServerState0),
{State, Actions} =
?HANDLE_EFFECTS(Effects, cast,
State0#state{server_state = ServerState,
monitors = Monitors}),
{keep_state, State, Actions};
error ->
{keep_state, State0, []}
end;
follower(info, {node_event, Node, down}, State) ->
case leader_id(State) of
{_, Node} ->
?INFO("~s: Leader node ~w may be down, setting pre-vote timeout",
[log_id(State), Node]),
{keep_state, State, [election_timeout_action(long, State)]};
_ ->
{keep_state, State}
end;
follower(info, {node_event, _Node, up}, State) ->
case leader_id(State) of
{_, Node} ->
?INFO("~s: Leader node ~w is back up, cancelling pre-vote timeout",
[log_id(State), Node]),
{keep_state, State,
[{state_timeout, infinity, election_timeout}]};
_ ->
{keep_state, State}
end;
follower(_, tick_timeout, State) ->
true = erlang:garbage_collect(),
_ = ets:insert(ra_metrics, ra_server:metrics(State#state.server_state)),
{keep_state, State, set_tick_timer(State, [])};
follower({call, From}, {log_fold, Fun, Term}, State) ->
fold_log(From, Fun, Term, State);
follower(EventType, Msg, #state{conf = Conf,
leader_monitor = MRef} = State0) ->
case handle_follower(Msg, State0) of
{follower, State1, Effects} ->
{State2, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = follower_leader_change(State0, State2),
{keep_state, State, maybe_set_election_timeout(State, Actions)};
{pre_vote, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
_ = stop_monitor(MRef),
{next_state, pre_vote, State#state{leader_monitor = undefined},
[election_timeout_action(long, State) | Actions]};
{await_condition, State1, Effects} ->
{State2, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = follower_leader_change(State0, State2),
{next_state, await_condition, State,
[{state_timeout, Conf#conf.await_condition_timeout,
await_condition_timeout}
| Actions]};
{receive_snapshot, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, receive_snapshot, State, Actions};
{delete_and_terminate, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, terminating_follower, State, Actions}
end.
%% TODO: handle leader down abort snapshot and revert to follower
receive_snapshot(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State,
[{state_timeout, receive_snapshot_timeout(), receive_snapshot_timeout}
| Actions]};
receive_snapshot(_, tick_timeout, State0) ->
{keep_state, State0, set_tick_timer(State0, [])};
receive_snapshot(EventType, Msg, State0) ->
case handle_receive_snapshot(Msg, State0) of
{receive_snapshot, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{keep_state, State,
[{state_timeout, receive_snapshot_timeout(),
receive_snapshot_timeout} | Actions]};
{follower, State1, Effects} ->
{State2, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = follower_leader_change(State0, State2),
{next_state, follower, State, Actions}
end.
terminating_leader(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
terminating_leader(_EvtType, {command, _, _}, State0) ->
% do not process any further commands
{keep_state, State0, []};
terminating_leader(EvtType, Msg, State0) ->
LogName = log_id(State0),
?DEBUG("~s: terminating leader received ~W~n", [LogName, Msg, 10]),
{keep_state, State, Actions} = leader(EvtType, Msg, State0),
NS = State#state.server_state,
case ra_server:is_fully_replicated(NS) of
true ->
{stop, {shutdown, delete}, State};
false ->
?DEBUG("~s: is not fully replicated after ~W~n",
[LogName, Msg, 7]),
{keep_state, send_rpcs(State), Actions}
end.
terminating_follower(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
terminating_follower(EvtType, Msg, State0) ->
% only process ra_log_events
{keep_state, State, Actions} = follower(EvtType, Msg, State0),
case ra_server:is_fully_persisted(State#state.server_state) of
true ->
{stop, {shutdown, delete}, State};
false ->
?DEBUG("~s: is not fully persisted after ~W~n", [log_id(State),
Msg, 7]),
{keep_state, State, Actions}
end.
await_condition({call, From}, {leader_call, Msg}, State) ->
maybe_redirect(From, Msg, State);
await_condition({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Reply = perform_local_query(QueryFun, follower, ServerState),
{keep_state, State, [{reply, From, Reply}]};
await_condition({call, From}, ping, State) ->
{keep_state, State, [{reply, From, {pong, await_condition}}]};
await_condition({call, From}, trigger_election, State) ->
{keep_state, State, [{reply, From, ok},
{next_event, cast, election_timeout}]};
await_condition(info, {'DOWN', MRef, process, _Pid, _Info},
State = #state{leader_monitor = MRef}) ->
?INFO("~s: Leader monitor down. Setting election timeout.",
[log_id(State)]),
{keep_state, State#state{leader_monitor = undefined},
[election_timeout_action(short, State)]};
await_condition(info, {node_event, Node, down}, State) ->
case leader_id(State) of
{_, Node} ->
?WARN("~s: Node ~w might be down. Setting election timeout.",
[log_id(State), Node]),
{keep_state, State, [election_timeout_action(long, State)]};
_ ->
{keep_state, State}
end;
await_condition(enter, OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, OldState, State0),
{keep_state, State, Actions};
await_condition(_, tick_timeout, State0) ->
{keep_state, State0, set_tick_timer(State0, [])};
await_condition(EventType, Msg, #state{leader_monitor = MRef} = State0) ->
case handle_await_condition(Msg, State0) of
{follower, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
NewState = follower_leader_change(State0, State),
{next_state, follower, NewState,
[{state_timeout, infinity, await_condition_timeout} |
maybe_set_election_timeout(State, Actions)]};
{pre_vote, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
_ = stop_monitor(MRef),
{next_state, pre_vote, State#state{leader_monitor = undefined},
[election_timeout_action(long, State) | Actions]};
{await_condition, State1, []} ->
{keep_state, State1, []}
end.
handle_event(_EventType, EventContent, StateName, State) ->
?WARN("~s: handle_event unknown ~P~n", [log_id(State), EventContent, 10]),
{next_state, StateName, State}.
terminate(Reason, StateName,
#state{conf = #conf{name = Key},
server_state = ServerState} = State) ->
?INFO("~s: terminating with ~w in state ~w~n",
[log_id(State), Reason, StateName]),
UId = uid(State),
_ = ra_server:terminate(ServerState, Reason),
Parent = ra_directory:where_is_parent(UId),
case Reason of
{shutdown, delete} ->
catch ra_directory:unregister_name(UId),
catch ra_log_meta:delete_sync(UId),
catch ets:delete(ra_state, UId),
Self = self(),
%% we have to terminate the child spec from the supervisor as it
%% wont do this automatically, even for transient children
%% for simple_one_for_one terminate also removes
_ = spawn(fun () ->
Ref = erlang:monitor(process, Self),
receive
{'DOWN', Ref, _, _, _} ->
ok = supervisor:terminate_child(
ra_server_sup_sup,
Parent)
after 5000 ->
ok
end
end),
ok;
_ -> ok
end,
_ = ets:delete(ra_metrics, Key),
_ = ets:delete(ra_state, Key),
ok.
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.
format_status(Opt, [_PDict, StateName, #state{server_state = NS}]) ->
[{id, ra_server:id(NS)},
{opt, Opt},
{raft_state, StateName},
{ra_server_state, ra_server:overview(NS)}
].
%%%===================================================================
%%% Internal functions
%%%===================================================================
handle_enter(RaftState, OldRaftState,
#state{conf = #conf{name = Name},
server_state = ServerState0} = State) ->
true = ets:insert(ra_state, {Name, RaftState}),
{ServerState, Effects} = ra_server:handle_state_enter(RaftState,
ServerState0),
case RaftState == leader orelse OldRaftState == leader of
true ->
%% ensure transitions from and to leader are logged at a higher
%% level
?NOTICE("~s: ~s -> ~s in term: ~b~n",
[log_id(State), OldRaftState, RaftState,
current_term(State)]);
false ->
?DEBUG("~s: ~s -> ~s in term: ~b~n",
[log_id(State), OldRaftState, RaftState,
current_term(State)])
end,
handle_effects(RaftState, Effects, cast,
State#state{server_state = ServerState}).
queue_take(N, Q) ->
queue_take(N, Q, []).
queue_take(0, Q, Acc) ->
{Q, lists:reverse(Acc)};
queue_take(N, Q0, Acc) ->
case queue:out(Q0) of
{{value, I}, Q} ->
queue_take(N-1, Q, [I | Acc]);
{empty, _} ->
{Q0, lists:reverse(Acc)}
end.
handle_leader(Msg, #state{server_state = ServerState0} = State0) ->
case catch ra_server:handle_leader(Msg, ServerState0) of
{NextState, ServerState, Effects} ->
State = State0#state{server_state =
ra_server:persist_last_applied(ServerState)},
{NextState, State, Effects};
OtherErr ->
?ERR("handle_leader err ~p~n", [OtherErr]),
exit(OtherErr)
end.
handle_candidate(Msg, #state{server_state = ServerState0} = State) ->
{NextState, ServerState, Effects} =
ra_server:handle_candidate(Msg, ServerState0),
{NextState, State#state{server_state = ServerState}, Effects}.
handle_pre_vote(Msg, #state{server_state = ServerState0} = State) ->
{NextState, ServerState, Effects} =
ra_server:handle_pre_vote(Msg, ServerState0),
{NextState, State#state{server_state = ServerState}, Effects}.
handle_follower(Msg, #state{server_state = ServerState0} = State0) ->
{NextState, ServerState, Effects} =
ra_server:handle_follower(Msg, ServerState0),
State = State0#state{server_state =
ra_server:persist_last_applied(ServerState)},
{NextState, State, Effects}.
handle_receive_snapshot(Msg, #state{server_state = ServerState0} = State) ->
{NextState, ServerState, Effects} =
ra_server:handle_receive_snapshot(Msg, ServerState0),
{NextState, State#state{server_state = ServerState}, Effects}.
handle_await_condition(Msg, #state{server_state = ServerState0} = State) ->
{NextState, ServerState, Effects} =
ra_server:handle_await_condition(Msg, ServerState0),
{NextState, State#state{server_state = ServerState}, Effects}.
perform_local_query(QueryFun, Leader, #{effective_machine_module := MacMod,
machine_state := MacState,
last_applied := Last,
current_term := Term}) ->
try ra_machine:query(MacMod, QueryFun, MacState) of
Result ->
{ok, {{Last, Term}, Result}, Leader}
catch
_:_ = Err ->
{error, Err}
end.
handle_effects(RaftState, Effects0, EvtType, State0) ->
handle_effects(RaftState, Effects0, EvtType, State0, []).
% effect handler: either executes an effect or builds up a list of
% gen_statem 'Actions' to be returned.
handle_effects(RaftState, Effects0, EvtType, State0, Actions0) ->
{State, Actions} = lists:foldl(
fun(Effects, {State, Actions}) when is_list(Effects) ->
handle_effects(RaftState, Effects, EvtType,
State, Actions);
(Effect, {State, Actions}) ->
handle_effect(RaftState, Effect, EvtType,
State, Actions)
end, {State0, Actions0}, Effects0),
{State, lists:reverse(Actions)}.
handle_effect(_, {send_rpc, To, Rpc}, _, State0, Actions) ->
% fully qualified use only so that we can mock it for testing
% TODO: review / refactor to remove the mod call here
?MODULE:send_rpc(To, Rpc),
{State0, Actions};
handle_effect(_, {next_event, Evt}, EvtType, State, Actions) ->
{State, [{next_event, EvtType, Evt} | Actions]};
handle_effect(_, {next_event, _, _} = Next, _, State, Actions) ->
{State, [Next | Actions]};
handle_effect(_, {send_msg, To, Msg}, _, State, Actions) ->
%% default is to send without any wrapping
ok = send(To, Msg),
{State, Actions};
handle_effect(_, {send_msg, To, Msg, Options}, _, State, Actions) ->
case parse_send_msg_options(Options) of
{true, true} ->
gen_cast(To, wrap_ra_event(State, machine, Msg));
{true, false} ->
send(To, wrap_ra_event(State, machine, Msg));
{false, true} ->
gen_cast(To, Msg);
{false, false} ->
send(To, Msg)
end,
{State, Actions};
handle_effect(RaftState, {aux, Cmd}, _, State, Actions) ->
%% TODO: thread through state
{_, ServerState, []} = ra_server:handle_aux(RaftState, cast, Cmd,
State#state.server_state),
{State#state{server_state = ServerState}, Actions};
handle_effect(_, {notify, Who, Correlations}, _, State, Actions) ->
ok = send_ra_event(Who, Correlations, applied, State),
{State, Actions};
handle_effect(_, {cast, To, Msg}, _, State, Actions) ->
ok = gen_cast(To, Msg),
{State, Actions};
handle_effect(_, {reply, From, Reply}, _, State, Actions) ->
% reply directly
ok = gen_statem:reply(From, Reply),
{State, Actions};
handle_effect(_, {reply, Reply}, {call, From}, State, Actions) ->
% reply directly
ok = gen_statem:reply(From, Reply),
{State, Actions};
handle_effect(_, {reply, Reply}, EvtType, _, _) ->
exit({undefined_reply, Reply, EvtType});
handle_effect(leader, {send_snapshot, To, {SnapState, Id, Term}}, _,
#state{server_state = SS0,
monitors = Monitors} = State0, Actions) ->
ChunkSize = application:get_env(ra, snapshot_chunk_size,
?DEFAULT_SNAPSHOT_CHUNK_SIZE),
%% leader effect only
Me = self(),
Pid = spawn(fun () ->
try send_snapshots(Me, Id, Term, To,
ChunkSize, SnapState) of
_ -> ok
catch
C:timeout:S ->
%% timeout is ok as we've already blocked
%% for a while
erlang:raise(C, timeout, S);
C:E:S ->
%% insert an arbitrary pause here as a primitive
%% throttling operation as certain errors
%% happen quickly
ok = timer:sleep(5000),
erlang:raise(C, E, S)
end
end),
MRef = erlang:monitor(process, Pid),
%% update the peer state so that no pipelined entries are sent during
%% the snapshot sending phase
SS = ra_server:update_peer_status(To, {sending_snapshot, Pid}, SS0),
{State0#state{server_state = SS,
monitors = Monitors#{Pid => {snapshot_sender, MRef}}},
Actions};
handle_effect(_, {delete_snapshot, Dir, SnapshotRef}, _, State0, Actions) ->
%% delete snapshots in separate process
_ = spawn(fun() ->
ra_snapshot:delete(Dir, SnapshotRef)
end),
{State0, Actions};
handle_effect(_, {send_vote_requests, VoteRequests}, _, % EvtType
State, Actions) ->
% transient election processes
T = {dirty_timeout, 500},
Me = self(),
[begin
_ = spawn(fun () -> Reply = gen_statem:call(N, M, T),
ok = gen_statem:cast(Me, Reply)
end)
end || {N, M} <- VoteRequests],
{State, Actions};
handle_effect(RaftState, {release_cursor, Index, MacState}, EvtType,
#state{server_state = ServerState0} = State0, Actions0) ->
{ServerState, Effects} = ra_server:update_release_cursor(Index, MacState,
ServerState0),
State1 = State0#state{server_state = ServerState},
handle_effects(RaftState, Effects, EvtType, State1, Actions0);
handle_effect(_, garbage_collection, _EvtType, State, Actions) ->
true = erlang:garbage_collect(),
{State, Actions};
handle_effect(_, {monitor, process, Pid}, _,
#state{monitors = Monitors} = State, Actions) ->
case Monitors of
#{Pid := _} ->
% monitor is already in place - do nothing
{State, Actions};
_ ->
MRef = erlang:monitor(process, Pid),
{State#state{monitors = Monitors#{Pid => {machine, MRef}}},
Actions}
end;
handle_effect(_, {monitor, process, Component, Pid}, _,
#state{monitors = Monitors} = State, Actions) ->
case Monitors of
#{Pid := _} ->
% monitor is already in place - do nothing
{State, Actions};
_ ->
MRef = erlang:monitor(process, Pid),
{State#state{monitors = Monitors#{Pid => {Component, MRef}}},
Actions}
end;
handle_effect(_, {monitor, node, Node}, _,
#state{monitors = Monitors} = State, Actions0) ->
case Monitors of
#{Node := _} ->
% monitor is already in place - do nothing
{State, Actions0};
_ ->
%% no need to actually monitor anything as we've always monitoring
%% all visible nodes
%% Fake a node event if the node is already connected so that the machine
%% can discover the current status
case lists:member(Node, nodes()) of
true ->
%% as effects get evaluated on state enter we cannot use
%% next_events
self() ! {nodeup, Node},
ok;
false ->
self() ! {nodedown, Node},
ok
end,
{State#state{monitors = Monitors#{Node => undefined}}, Actions0}
end;
handle_effect(_, {demonitor, process, Pid}, _,
#state{monitors = Monitors0} = State, Actions) ->
case maps:take(Pid, Monitors0) of
{{_, MRef}, Monitors} ->
true = erlang:demonitor(MRef),
{State#state{monitors = Monitors}, Actions};
error ->
% ref not known - do nothing
{State, Actions}
end;
handle_effect(_, {demonitor, node, Node}, _,
#state{monitors = Monitors0} = State, Actions) ->
case maps:take(Node, Monitors0) of
{_, Monitors} ->
{State#state{monitors = Monitors}, Actions};
error ->
{State, Actions}
end;
handle_effect(_, {incr_metrics, Table, Ops}, _,
State = #state{conf = #conf{name = Key}}, Actions) ->
_ = ets:update_counter(Table, Key, Ops),
{State, Actions};
handle_effect(_, {timer, Name, T}, _, State, Actions) ->
{State, [{{timeout, Name}, T, machine_timeout} | Actions]};
handle_effect(RaftState, {log, Idxs, Fun}, EvtType,
State = #state{server_state = SS0}, Actions) when is_list(Idxs) ->
%% Useful to implement a batch send of data obtained from the log.
%% 1) Retrieve all data from the list of indexes
{Data, SS} = lists:foldl(fun(Idx, {Data0, Acc0}) ->
case ra_server:read_at(Idx, Acc0) of
{ok, D, Acc} ->
{[D | Data0], Acc};
{error, _} ->
%% this is unrecoverable
exit({failed_to_read_index_for_log_effect,
Idx})
end
end, {[], SS0}, Idxs),
%% 2) Apply the fun to the list of data as a whole and deal with any effects
case Fun(lists:reverse(Data)) of
[] ->
{State#state{server_state = SS}, Actions};
Effects ->
%% recurse with the new effects
handle_effects(RaftState, Effects, EvtType,
State#state{server_state = SS}, Actions)
end;
handle_effect(_, {mod_call, Mod, Fun, Args}, _,
State, Actions) ->
_ = erlang:apply(Mod, Fun, Args),
{State, Actions}.
send_rpcs(State0) ->
{State, Rpcs} = make_rpcs(State0),
% module call so that we can mock
% TODO: review
[ok = ?MODULE:send_rpc(To, Rpc) || {send_rpc, To, Rpc} <- Rpcs],
State.
make_rpcs(State) ->
{ServerState, Rpcs} = ra_server:make_rpcs(State#state.server_state),
{State#state{server_state = ServerState}, Rpcs}.
send_rpc(To, Msg) ->
% fake gen cast - need to avoid any blocking delays here
gen_cast(To, Msg).
gen_cast(To, Msg) ->
send(To, {'$gen_cast', Msg}).
send_ra_event(To, Msg, EvtType, State) ->
send(To, wrap_ra_event(State, EvtType, Msg)).
wrap_ra_event(#state{conf = #conf{ra_event_formatter = undefined}} = State,
EvtType, Evt) ->
{ra_event, id(State), {EvtType, Evt}};
wrap_ra_event(#state{conf = #conf{ra_event_formatter = {M, F, A}}} = State,
EvtType, Evt) ->
apply(M, F, [id(State), {EvtType, Evt} | A]).
parse_send_msg_options(ra_event) ->
{true, false};
parse_send_msg_options(cast) ->
{false, true};
parse_send_msg_options(Options) when is_list(Options) ->
{lists:member(ra_event, Options), lists:member(cast, Options)}.
id(#state{server_state = ServerState}) ->
ra_server:id(ServerState).
log_id(#state{conf = #conf{log_id = N}}) ->
N.
uid(#state{server_state = ServerState}) ->
ra_server:uid(ServerState).
leader_id(#state{server_state = ServerState}) ->
ra_server:leader_id(ServerState).
current_term(#state{server_state = ServerState}) ->
ra_server:current_term(ServerState).
process_pending_queries(NewLeader,
#state{server_state = ServerState0} = State) ->
{ServerState, Froms} = ra_server:process_new_leader_queries(ServerState0),
[_ = gen_statem:reply(F, {redirect, NewLeader})
|| F <- Froms],
State#state{server_state = ServerState}.
maybe_set_election_timeout(#state{leader_monitor = LeaderMon},
Actions) when LeaderMon =/= undefined ->
% only when a leader is known should we cancel the election timeout
[{state_timeout, infinity, election_timeout} | Actions];
maybe_set_election_timeout(State, Actions) ->
[election_timeout_action(short, State) | Actions].
election_timeout_action(Length, #state{conf = Conf}) ->
election_timeout_action(Length, Conf);
election_timeout_action(really_short, #conf{broadcast_time = Timeout}) ->
T = rand:uniform(Timeout),
{state_timeout, T, election_timeout};
election_timeout_action(short, #conf{broadcast_time = Timeout}) ->
T = rand:uniform(Timeout * ?DEFAULT_ELECTION_MULT) + Timeout,
{state_timeout, T, election_timeout};
election_timeout_action(long, #conf{broadcast_time = Timeout}) ->
%% this should be longer than aten detection poll interval
T = rand:uniform(Timeout * ?DEFAULT_ELECTION_MULT * 2) + 1000,
{state_timeout, T, election_timeout}.
% sets the tick timer for periodic actions such as sending rpcs to servers
% that are stale to ensure liveness
set_tick_timer(#state{conf = #conf{tick_timeout = TickTimeout}}, Actions) ->
[{{timeout, tick}, TickTimeout, tick_timeout} | Actions].
follower_leader_change(Old, #state{pending_commands = Pending,
leader_monitor = OldMRef} = New) ->
OldLeader = leader_id(Old),
case leader_id(New) of
OldLeader ->
% no change
New;
undefined ->
New;
NewLeader ->
MRef = swap_monitor(OldMRef, NewLeader),
LeaderNode = ra_lib:ra_server_id_node(NewLeader),
ok = aten_register(LeaderNode),
OldLeaderNode = ra_lib:ra_server_id_node(OldLeader),
ok = aten:unregister(OldLeaderNode),
% leader has either changed or just been set
?INFO("~s: detected a new leader ~w in term ~b~n",
[log_id(New), NewLeader, current_term(New)]),
[ok = gen_statem:reply(From, {redirect, NewLeader})
|| {From, _Data} <- Pending],
process_pending_queries(NewLeader,
New#state{pending_commands = [],
leader_monitor = MRef})
end.
aten_register(Node) ->
case node() of
Node -> ok;
_ ->
aten:register(Node)
end.
swap_monitor(MRef, L) ->
stop_monitor(MRef),
erlang:monitor(process, L).
stop_monitor(undefined) ->
ok;
stop_monitor(MRef) ->
erlang:demonitor(MRef),
ok.
gen_statem_safe_call(ServerId, Msg, Timeout) ->
try
gen_statem:call(ServerId, Msg, Timeout)
catch
exit:{timeout, _} ->
timeout;
exit:{noproc, _} ->
{error, noproc};
exit:{{nodedown, _}, _} ->
{error, nodedown}
end.
do_state_query(all, State) -> State;
do_state_query(machine, #{machine_state := MacState}) ->
MacState;
do_state_query(members, #{cluster := Cluster}) ->
maps:keys(Cluster).
config_defaults() ->
#{broadcast_time => ?DEFAULT_BROADCAST_TIME,
tick_timeout => ?TICK_INTERVAL_MS,
await_condition_timeout => ?DEFAULT_AWAIT_CONDITION_TIMEOUT,
initial_members => []
}.
maybe_redirect(From, Msg, #state{pending_commands = Pending,
leader_monitor = LeaderMon} = State) ->
Leader = leader_id(State),
case LeaderMon of
undefined ->
?INFO("~s: leader call - leader not known. "
"Command will be forwarded once leader is known.~n",
[log_id(State)]),
{keep_state,
State#state{pending_commands = [{From, Msg} | Pending]}};
_ when Leader =/= undefined ->
{keep_state, State, {reply, From, {redirect, Leader}}}
end.
reject_command(Pid, Corr, State) ->
LeaderId = leader_id(State),
case LeaderId of
undefined ->
%% don't log these as they may never be resolved
ok;
_ ->
?INFO("~s: follower received leader command from ~w. "
"Rejecting to ~w ~n", [log_id(State), Pid, LeaderId])
end,
send_ra_event(Pid, {not_leader, LeaderId, Corr}, rejected, State).
maybe_persist_last_applied(#state{server_state = NS} = State) ->
State#state{server_state = ra_server:persist_last_applied(NS)}.
send(To, Msg) ->
% we do not want to block the ra server whilst attempting to set up
% a TCP connection to a potentially down node.
case erlang:send(To, Msg, [noconnect, nosuspend]) of
ok -> ok;
_ -> ok
end.
fold_log(From, Fun, Term, State) ->
case ra_server:log_fold(State#state.server_state, Fun, Term) of
{ok, Result, ServerState} ->
{keep_state, State#state{server_state = ServerState},
[{reply, From, {ok, Result}}]};
{error, Reason, ServerState} ->
{keep_state, State#state{server_state = ServerState},
[{reply, From, {error, Reason}}]}
end.
maybe_add_election_timeout(election_timeout, Actions, State) ->
[election_timeout_action(long, State) | Actions];
maybe_add_election_timeout(_, Actions, _) ->
Actions.
receive_snapshot_timeout() ->
application:get_env(ra, receive_snapshot_timeout,
?DEFAULT_RECEIVE_SNAPSHOT_TIMEOUT).
send_snapshots(Me, Id, Term, To, ChunkSize, SnapState) ->
{ok,
Meta,
ReadState} = ra_snapshot:begin_read(SnapState),
RPC = #install_snapshot_rpc{term = Term,
leader_id = Id,
meta = Meta},
Result = read_chunks_and_send_rpc(RPC, To, ReadState, 1,
ChunkSize, SnapState),
ok = gen_statem:cast(Me, {To, Result}).
read_chunks_and_send_rpc(RPC0,
To, ReadState0, Num, ChunkSize, SnapState) ->
{ok, Data, ContState} = ra_snapshot:read_chunk(ReadState0, ChunkSize,
SnapState),
ChunkFlag = case ContState of
{next, _} -> next;
last -> last
end,
%% TODO: some of the metadata, e.g. the cluster is redundant in subsequent
%% rpcs
RPC1 = RPC0#install_snapshot_rpc{chunk_state = {Num, ChunkFlag},
data = Data},
Res1 = gen_statem:call(To, RPC1,
{dirty_timeout, ?INSTALL_SNAP_RPC_TIMEOUT}),
case ContState of
{next, ReadState1} ->
read_chunks_and_send_rpc(RPC0, To, ReadState1, Num + 1,
ChunkSize, SnapState);
last ->
Res1
end.
make_command(Type, {call, From}, Data, Mode) ->
Ts = os:system_time(millisecond),
{Type, #{from => From, ts => Ts}, Data, Mode};
make_command(Type, _, Data, Mode) ->
Ts = os:system_time(millisecond),
{Type, #{ts => Ts}, Data, Mode}.