Packages
ra
0.9.2
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,
follower/3,
pre_vote/3,
candidate/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
]).
-export([send_rpc/2]).
-define(SERVER, ?MODULE).
-define(DEFAULT_BROADCAST_TIME, 50).
-define(DEFAULT_ELECTION_MULT, 3).
-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(HANDLE_EFFECTS(Effects, EvtType, State0),
handle_effects(?FUNCTION_NAME, Effects, EvtType, State0)).
-type query_fun() :: fun((term()) -> term()).
-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.
-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]).
-record(state, {server_state :: ra_server:ra_server_state(),
name :: atom(),
broadcast_time = ?DEFAULT_BROADCAST_TIME :: non_neg_integer(),
tick_timeout :: non_neg_integer(),
monitors = #{} :: #{pid() | node() => maybe(reference())},
pending_commands = [] :: [{{pid(), any()}, term()}],
leader_monitor :: reference() | undefined,
await_condition_timeout :: non_neg_integer(),
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(ra_server_id(), ra_command(), timeout()) ->
ra_cmd_ret().
command(ServerRef, Cmd, Timeout) ->
leader_call(ServerRef, {command, normal, Cmd}, Timeout).
-spec cast_command(ra_server_id(), ra_command()) -> ok.
cast_command(ServerRef, Cmd) ->
gen_statem:cast(ServerRef, {command, low, Cmd}).
-spec cast_command(ra_server_id(), ra_command_priority(), ra_command()) -> ok.
cast_command(ServerRef, Priority, Cmd) ->
gen_statem:cast(ServerRef, {command, Priority, Cmd}).
-spec query(ra_server_id(), query_fun(), local | consistent, timeout()) ->
{ok, term(), ra_server_id()}.
query(ServerRef, QueryFun, local, Timeout) ->
gen_statem:call(ServerRef, {local_query, QueryFun}, Timeout);
query(ServerRef, QueryFun, consistent, Timeout) ->
% TODO: timeout
command(ServerRef, {'$ra_query', QueryFun, await_consensus}, Timeout).
-spec log_fold(ra_server_id(), fun(), term(), integer()) -> term().
log_fold(ServerRef, Fun, InitialState, Timeout) ->
gen_statem:call(ServerRef, {log_fold, Fun, InitialState}, Timeout).
%% used to query the raft state rather than the machine state
-spec state_query(ra_server_id(), all | members | machine, timeout()) ->
ra_leader_call_ret(term()).
state_query(ServerRef, Spec, Timeout) ->
leader_call(ServerRef, {state_query, Spec}, Timeout).
-spec trigger_election(ra_server_id(), timeout()) -> ok.
trigger_election(ServerRef, Timeout) ->
gen_statem:call(ServerRef, trigger_election, Timeout).
-spec ping(ra_server_id(), timeout()) -> safe_call_ret({pong, states()}).
ping(ServerRef, Timeout) ->
gen_statem_safe_call(ServerRef, ping, Timeout).
leader_call(ServerRef, Msg, Timeout) ->
case gen_statem_safe_call(ServerRef, {leader_call, Msg},
Timeout) of
{redirect, Leader} ->
leader_call(Leader, Msg, Timeout);
{machine_reply, Reply} ->
{ok, Reply, ServerRef};
{error, _} = E ->
E;
timeout ->
% TODO: formatted error message
{timeout, ServerRef};
Reply ->
{ok, Reply, ServerRef}
end.
%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================
init(Config0) when is_map(Config0) ->
process_flag(trap_exit, true),
Config = maps:merge(config_defaults(), Config0),
#{id := Id, uid := UId,
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(), Key),
_ = ets:insert(ra_metrics, {Key, 0, 0}),
% ensure each relevant erlang node is connected
Peers = maps:keys(maps:remove(Id, Cluster)),
_ = 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),
State = #state{server_state = ServerState, name = Key,
tick_timeout = TickTime,
await_condition_timeout = AwaitCondTimeout},
%% TODO: this should really be a {next_event, cast, go} but OTP 20.3
%% does not support this. it was fixed in 20.3.2
%% monitor nodes so that we can handle both nodeup and nodedown events
ok = net_kernel:monitor_nodes(true),
{ok, recover, State, [{state_timeout, 0, go}]}.
%% callback mode
callback_mode() -> [state_functions, state_enter].
%%%===================================================================
%%% State functions
%%%===================================================================
recover(enter, _OldState, State) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, State),
{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, State) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, State),
{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 ->
?INFO("~w: is not new, setting election timeout.~n",
[id(State)]),
[election_timeout_action(short, State)]
end,
{next_state, follower, State, set_tick_timer(State, Actions)}.
leader(enter, _, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, 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
From = case EventType of
cast -> undefined;
{call, F} -> F
end,
{leader, ServerState, Effects} =
ra_server:handle_leader({command, {CmdType, #{from => From},
Data, ReplyMode}},
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
From = case EventType of
cast -> undefined;
{call, F} -> F
end,
Cmd = {CmdType, #{from => From}, 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{server_state = ServerState0,
delayed_commands = Delayed0} = State0) ->
{DelQ, Delayed} = queue_take(25, 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, leader, ServerState),
{keep_state, State, [{reply, From, Reply}]};
leader({call, From}, {state_query, Spec},
#state{server_state = ServerState} = State) ->
Reply = do_state_query(Spec, ServerState),
{keep_state, State, [{reply, From, Reply}]};
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
{MRef, Monitors} ->
% there is a monitor for the ref
{leader, ServerState, Effects} =
ra_server:handle_leader({command,
{'$usr', #{from => undefined},
{down, Pid, Info}, noreply}},
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
{leader, ServerState, Effects} =
ra_server:handle_leader({command,
{'$usr', #{from => undefined},
{NodeEvt, Node}, noreply}},
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 = maybe_persist_last_applied(send_rpcs(State0)),
Effects = ra_server:tick(State1#state.server_state),
{State, Actions} = ?HANDLE_EFFECTS(Effects, cast, State1),
true = erlang:garbage_collect(),
{keep_state, State, set_tick_timer(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, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{keep_state, State, Actions};
{follower, State1, Effects} ->
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
% demonitor when stepping down
ok = lists:foreach(fun (Ref) when is_reference(Ref) ->
erlang:demonitor(Ref);
(_) ->
%% the monitor is a node
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 ->
?INFO("~w leader -> terminating_leader term: ~b~n",
[id(State), current_term(State)]),
{next_state, terminating_leader, State, Actions}
end
end.
candidate(enter, _, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, 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_on_consensus, 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, candidate, 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),
{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),
?INFO("~w candidate -> follower term: ~b~n",
[id(State), current_term(State)]),
{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
% TODO: only set this is leader was not detected
[election_timeout_action(long, State) | Actions]};
{leader, State1, Effects} ->
{State2, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
State = State2#state{pending_commands = []},
% inject a bunch of command events to be processed when node
% becomes leader
NextEvents = [{next_event, {call, F}, Cmd} || {F, Cmd} <- Pending],
?INFO("~w candidate -> leader term: ~b~n",
[id(State), current_term(State)]),
{next_state, leader, State, Actions ++ NextEvents}
end.
pre_vote(enter, _, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, 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_on_consensus, 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, pre_vote, 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),
{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),
?INFO("~w pre_vote -> follower term: ~b~n",
[id(State), current_term(State)]),
{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),
?INFO("~w pre_vote -> candidate term: ~b~n",
[id(State), current_term(State)]),
{next_state, candidate, State,
[election_timeout_action(long, State) | Actions]}
end.
follower(enter, _, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, 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("~w leader cast - leader not known. "
"Command is dropped.~n", [id(State)]),
{keep_state, State, []};
LeaderId ->
?INFO("~w follower leader cast - redirecting to ~w ~n",
[id(State), LeaderId]),
ok = ra:pipeline_command(LeaderId, Data, no_correlation, Priority),
{keep_state, State, []}
end;
follower(cast, {command, _Priority,
{_CmdType, _Data, {notify_on_consensus, Corr, Pid}}},
State) ->
ok = reject_command(Pid, Corr, State),
{keep_state, State, []};
follower({call, From}, {local_query, QueryFun},
#state{server_state = ServerState} = State) ->
Reply = perform_local_query(QueryFun, follower, ServerState),
{keep_state, State, [{reply, From, Reply}]};
follower({call, From}, trigger_election, State) ->
?INFO("~w: election triggered by ~w", [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) ->
?WARN("~w: Leader monitor down with ~W, setting election timeout~n",
[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, {node_event, Node, down}, State) ->
case leader_id(State) of
{_, Node} ->
?WARN("~w: Leader node ~w may be down, setting election timeout",
[id(State), Node]),
{keep_state, State, [election_timeout_action(long, State)]};
_ ->
{keep_state, State}
end;
follower(_, tick_timeout, State0) ->
State = maybe_persist_last_applied(State0),
true = erlang:garbage_collect(),
{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{await_condition_timeout = AwaitCondTimeout,
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),
?INFO("~w follower -> pre_vote in term: ~b~n",
[id(State), current_term(State)]),
_ = 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),
?INFO("~w follower -> await_condition term: ~b~n",
[id(State), current_term(State)]),
{next_state, await_condition, State,
[{state_timeout, AwaitCondTimeout, await_condition_timeout}
| Actions]};
{delete_and_terminate, State1, Effects} ->
?INFO("~w follower -> terminating_follower term: ~b~n",
[id(State1), current_term(State1)]),
{State, Actions} = ?HANDLE_EFFECTS(Effects, EventType, State1),
{next_state, terminating_follower, State, Actions}
end.
terminating_leader(enter, _, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, State0),
{keep_state, State, Actions};
terminating_leader(_EvtType, {command, _, _}, State0) ->
% do not process any further commands
{keep_state, State0, []};
terminating_leader(EvtType, Msg, State0) ->
?INFO("terminating leader got ~w~n", [Msg]),
{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 ->
?INFO("~w: is not fully replicated after ~W~n", [id(State),
Msg, 7]),
{keep_state, State, Actions}
end.
terminating_follower(enter, _OldState, State0) ->
{State, Actions} = handle_enter(?FUNCTION_NAME, 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 ->
?INFO("~w: is not fully persisted after ~W~n", [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, name = Name}) ->
?WARN("~p: Leader monitor down. Setting election timeout.", [Name]),
{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("~p: Node ~p might be down. Setting election timeout.",
[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, State0),
{keep_state, State, Actions};
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),
?INFO("~w await_condition -> follower in term: ~b~n",
[id(State), current_term(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),
?INFO("~w await_condition -> pre_vote in term: ~b~n",
[id(State), current_term(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("~p: handle_event unknown ~p~n", [id(State), EventContent]),
{next_state, StateName, State}.
terminate(Reason, StateName,
#state{name = Key, server_state = ServerState} = State) ->
?WARN("ra: ~w terminating with ~w in state ~w~n",
[id(State), Reason, StateName]),
UId = uid(State),
_ = ra_server:terminate(ServerState, Reason),
case Reason of
{shutdown, delete} ->
catch ra_log_segment_writer:release_segments(
ra_log_segment_writer, UId),
catch ra_directory:unregister_name(UId),
catch ra_log_meta:delete_sync(UId);
_ -> ok
end,
_ = aten:unregister(Key),
_ = 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, #state{name = Name,
server_state = ServerState0} = State) ->
true = ets:insert(ra_state, {Name, RaftState}),
{ServerState, Effects} = ra_server:handle_state_enter(RaftState,
ServerState0),
% ?INFO("state change effects ~w", [Effects]),
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} = State) ->
case catch ra_server:handle_leader(Msg, ServerState0) of
{NextState, ServerState, Effects} ->
{NextState, State#state{server_state = ServerState}, Effects}
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} = State) ->
{NextState, ServerState, Effects} = ra_server:handle_follower(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, #{machine := {machine, MacMod, _},
machine_state := MacState,
last_applied := Last,
id := Leader,
current_term := Term}) ->
{ok, {{Last, Term}, ra_machine:query(MacMod, QueryFun, MacState)}, Leader};
perform_local_query(QueryFun, _, #{machine := {machine, MacMod, _},
machine_state := MacState,
last_applied := Last,
current_term := Term} = ServerState) ->
Leader = maps:get(leader_id, ServerState, not_known),
{ok, {{Last, Term}, ra_machine:query(MacMod, QueryFun, MacState)}, Leader}.
% effect handler: either executes an effect or builds up a list of
% gen_statem 'Actions' to be returned.
handle_effects(RaftState, Effects, EvtType, State0) ->
lists:foldl(fun(Effect, {State, Actions}) ->
handle_effect(RaftState, Effect, EvtType,
State, Actions)
end, {State0, []}, Effects).
handle_effect(_, {send_rpcs, Rpcs}, _, State0, Actions) ->
% fully qualified use only so that we can mock it for testing
% TODO: review / refactor
[?MODULE:send_rpc(To, Rpc) || {To, Rpc} <- Rpcs],
% TODO: record metrics for sending times
{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(id(State), machine, Msg));
{true, false} ->
send(To, wrap_ra_event(id(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}, _, _, _) ->
exit({undefined_reply, Reply});
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(_, {release_cursor, Index, MacState}, _EvtType,
#state{server_state = ServerState0} = State, Actions) ->
ServerState = ra_server:update_release_cursor(Index, MacState, ServerState0),
{State#state{server_state = ServerState}, Actions};
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 => MRef}}, Actions}
end;
handle_effect(_, {monitor, node, Node}, _,
#state{monitors = Monitors} = State, Actions) ->
case Monitors of
#{Node := _} ->
% monitor is already in place - do nothing
{State, Actions};
_ ->
%% no need to actually monitor anything as we've always monitoring
%% all visible nodes
{State#state{monitors = Monitors#{Node => undefined}}, Actions}
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{name = Key}, Actions) ->
_ = ets:update_counter(Table, Key, Ops),
{State, Actions};
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) || {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(id(State), EvtType, Msg)).
wrap_ra_event(ServerId, EvtType, Evt) ->
{ra_event, ServerId, {EvtType, Evt}}.
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).
uid(#state{server_state = ServerState}) ->
ra_server:uid(ServerState).
leader_id(#state{server_state = ServerState}) ->
ra_server:leader_id(ServerState).
-ifdef(info).
current_term(#state{server_state = ServerState}) ->
ra_server:current_term(ServerState).
-endif.
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(really_short, #state{broadcast_time = Timeout}) ->
T = rand:uniform(Timeout * ?DEFAULT_ELECTION_MULT),
{state_timeout, T, election_timeout};
election_timeout_action(short, #state{broadcast_time = Timeout}) ->
T = rand:uniform(Timeout * ?DEFAULT_ELECTION_MULT) + (Timeout * 2),
{state_timeout, T, election_timeout};
election_timeout_action(long, #state{broadcast_time = Timeout}) ->
T = rand:uniform(Timeout * ?DEFAULT_ELECTION_MULT) + (Timeout * 4),
{state_timeout, T, election_timeout}.
% sets the tock timer for periodic actions such as sending stale rpcs
% or persisting the last_applied index
set_tick_timer(#state{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("~w detected a new leader ~w in term ~b~n",
[id(New), NewLeader, current_term(New)]),
[ok = gen_statem:reply(From, {redirect, NewLeader})
|| {From, _Data} <- Pending],
New#state{pending_commands = [],
leader_monitor = MRef}
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(ServerRef, Msg, Timeout) ->
try
gen_statem:call(ServerRef, 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 ->
?WARN("~w leader call - leader not known. "
"Command will be forwarded once leader is known.~n",
[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),
?INFO("~w: follower received leader command - rejecting to ~w ~n",
[id(State), LeaderId]),
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.