Current section
Files
Jump to
Current section
Files
src/riak_ensemble_msg.erl
%% -------------------------------------------------------------------
%%
%% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% TODO: Before PR. Add module edoc + doc functions
-module(riak_ensemble_msg).
-export([send_all/4,
send_all/5,
blocking_send_all/4,
blocking_send_all/5,
blocking_send_all/6,
wait_for_quorum/1,
cast_all/3,
handle_reply/4,
quorum_timeout/1,
reply/3]).
-include_lib("riak_ensemble_types.hrl").
%% -define(OUT(Fmt,Args), io:format(Fmt,Args)).
-define(OUT(Fmt,Args), ok).
%%%===================================================================
-type required() :: quorum | other | all | all_or_quorum.
-record(msgstate, {awaiting = undefined :: 'undefined' | reqid(),
timer = undefined :: 'undefined' | reference(),
required = quorum :: required(),
id :: peer_id(),
views = [] :: views(),
replies = [] :: [{peer_id(), any()}]}).
-opaque msg_state() :: #msgstate{}.
-export_type([msg_state/0]).
-type timer() :: reference().
-type reqid() :: reference().
-type msg() :: term().
-type peer_nack() :: {peer_id(), nack}.
-type msg_from() :: {riak_ensemble_msg,pid(),reference()}.
-type from() :: {pid(),reference()}.
-type maybe_from() :: undefined | from().
-type future() :: undefined | pid().
-export_type([future/0, msg_from/0]).
-type extra_check() :: undefined | fun(([peer_reply()]) -> boolean()).
-export_type([extra_check/0]).
-record(collect, {replies :: [peer_reply()],
parent :: maybe_from(),
id :: peer_id(),
views :: views(),
required :: required(),
extra :: extra_check(),
reqid :: reqid()
}).
-type collect() :: #collect{}.
%%%===================================================================
-spec send_all(msg(), peer_id(), peer_pids(), views()) -> msg_state().
send_all(Msg, Id, Peers, Views) ->
send_all(Msg, Id, Peers, Views, quorum).
-spec send_all(msg(), peer_id(), peer_pids(), views(), required()) -> msg_state().
send_all(_Msg, Id, _Peers=[{Id,_}], _Views, _Required) ->
?OUT("~p: self-sending~n", [Id]),
gen_fsm:send_event(self(), {quorum_met, []}),
#msgstate{awaiting=undefined, timer=undefined, replies=[], id=Id};
send_all(Msg, Id, Peers, Views, Required) ->
?OUT("~p/~p: sending to ~p: ~p~n", [Id, self(), Peers, Msg]),
{ReqId, Request} = make_request(Msg),
_ = [maybe_send_request(Id, Peer, ReqId, Request) || Peer={PeerId,_} <- Peers,
PeerId =/= Id],
Timer = send_after(?ENSEMBLE_TICK, self(), quorum_timeout),
#msgstate{awaiting=ReqId, timer=Timer, replies=[], id=Id, views=Views,
required=Required}.
%%%===================================================================
-spec cast_all(msg(), peer_id(), peer_pids()) -> ok.
cast_all(Msg, Id, Peers) ->
?OUT("~p/~p: casting to ~p: ~p~n", [Id, self(), Peers, Msg]),
_ = [maybe_send_cast(Id, Peer, Msg) || Peer={PeerId,_} <- Peers,
PeerId =/= Id],
ok.
%%%===================================================================
-spec maybe_send_request(peer_id(), {peer_id(), maybe_pid()}, reqid(), msg()) -> ok.
-ifdef(TEST).
maybe_send_request(Id, {PeerId, PeerPid}, ReqId, Event) ->
case riak_ensemble_test:maybe_drop(Id, PeerId) of
true ->
%% TODO: Consider nacking instead
io:format("Dropping ~p -> ~p~n", [Id, PeerId]),
ok;
false ->
send_request({PeerId, PeerPid}, ReqId, Event)
end.
-else.
maybe_send_request(_Id, {PeerId, PeerPid}, ReqId, Event) ->
send_request({PeerId, PeerPid}, ReqId, Event).
-endif.
%%%===================================================================
-spec send_request({peer_id(), maybe_pid()}, reqid(), msg()) -> ok.
send_request({PeerId, PeerPid}, ReqId, Event) ->
case PeerPid of
undefined ->
?OUT("~p: Sending offline nack for ~p~n", [self(), PeerId]),
From = make_from(self(), ReqId),
reply(From, PeerId, nack);
_ ->
?OUT("~p: Sending to ~p: ~p~n", [self(), PeerId, Event]),
gen_fsm:send_event(PeerPid, Event)
end.
%%%===================================================================
-spec maybe_send_cast(peer_id(), {peer_id(), maybe_pid()}, msg()) -> ok.
-ifdef(TEST).
maybe_send_cast(Id, {PeerId, PeerPid}, Event) ->
case riak_ensemble_test:maybe_drop(Id, PeerId) of
true ->
%% TODO: Consider nacking instead
io:format("Dropping ~p -> ~p~n", [Id, PeerId]),
ok;
false ->
send_cast({PeerId, PeerPid}, Event)
end.
-else.
maybe_send_cast(_Id, {PeerId, PeerPid}, Event) ->
send_cast({PeerId, PeerPid}, Event).
-endif.
%%%===================================================================
-spec send_cast({peer_id(), maybe_pid()}, msg()) -> ok.
send_cast({_PeerId, PeerPid}, Event) ->
case PeerPid of
undefined ->
ok;
_ ->
?OUT("~p: Sending to ~p: ~p~n", [self(), _PeerId, Event]),
gen_fsm:send_event(PeerPid, Event)
end.
%%%===================================================================
-spec reply(msg_from(), peer_id(), any()) -> ok.
reply({riak_ensemble_msg, Sender, ReqId}, Id, Reply) ->
gen_fsm:send_all_state_event(Sender, {reply, ReqId, Id, Reply}).
%%%===================================================================
-spec blocking_send_all(msg(), peer_id(), peer_pids(), views())
-> {future(), msg_state()}.
blocking_send_all(Msg, Id, Peers, Views) ->
blocking_send_all(Msg, Id, Peers, Views, quorum, undefined).
-spec blocking_send_all(msg(), peer_id(), peer_pids(), views(), required())
-> {future(), msg_state()}.
blocking_send_all(Msg, Id, Peers, Views, Required) when Required =/= undefined ->
blocking_send_all(Msg, Id, Peers, Views, Required, undefined).
-spec blocking_send_all(msg(), peer_id(), peer_pids(), views(),
required(), extra_check()) -> {future(), msg_state()}.
blocking_send_all(Msg, Id, Peers, Views, Required, Extra) when Required =/= undefined ->
?OUT("~p: blocking_send_all to ~p: ~p~n", [Id, Peers, Msg]),
MsgState = #msgstate{awaiting=undefined, timer=undefined, replies=[],
views=Views, id=Id, required=Required},
Future = case Peers of
[{Id,_}] ->
undefined;
_ ->
spawn_link(fun() ->
collector(Msg, Peers, Extra, MsgState)
end)
end,
{Future, MsgState}.
-spec collector(msg(), peer_pids(), extra_check(), msg_state()) -> ok.
collector(Msg, Peers, Extra, #msgstate{id=Id, views=Views, required=Required}) ->
{ReqId, Request} = make_request(Msg),
_ = [maybe_send_request(Id, Peer, ReqId, Request) || Peer={PeerId,_} <- Peers,
PeerId =/= Id],
collect_replies(#collect{replies=[],
parent=undefined,
id=Id,
views=Views,
required=Required,
extra=Extra,
reqid=ReqId}).
-spec collect_replies(collect()) -> ok.
collect_replies(Collect=#collect{replies=Replies, reqid=ReqId}) ->
receive
{'$gen_all_state_event', Event} ->
{reply, ReqId, Peer, Reply} = Event,
Replies2 = [{Peer, Reply}|Replies],
check_enough(Collect#collect{replies=Replies2});
{waiting, From, Ref} when is_pid(From), is_reference(Ref) ->
Parent = {From, Ref},
check_enough(Collect#collect{parent=Parent})
after ?ENSEMBLE_TICK ->
maybe_timeout(Collect)
end.
maybe_timeout(#collect{parent=undefined, replies=Replies, id=Id,
views=Views, required=Required, extra=Extra}) ->
receive {waiting, From, Ref} ->
case quorum_met(Replies, Id, Views, Required, Extra) of
true ->
From ! {Ref, ok, Replies},
ok;
_ ->
collect_timeout(Replies, {From, Ref})
end
end;
maybe_timeout(#collect{replies=Replies, parent=Parent}) ->
collect_timeout(Replies, Parent).
-spec collect_timeout([peer_reply()], from()) -> ok.
collect_timeout(Replies, {From, Ref}) ->
From ! {Ref, timeout, Replies},
ok.
-spec check_enough(collect()) -> ok.
check_enough(Collect=#collect{parent=undefined}) ->
collect_replies(Collect);
check_enough(Collect=#collect{id=Id,
replies=Replies,
parent={From,Ref}=Parent,
views=Views,
required=Required,
extra=Extra}) ->
case quorum_met(Replies, Id, Views, Required, Extra) of
true when Required =:= all_or_quorum ->
%% If we've hit a quorum with all_or_quorum required, then
%% we need to wait some additional length of time and see
%% if we get replies from all.
try_collect_all(Collect);
true ->
From ! {Ref, ok, Replies},
ok;
nack ->
collect_timeout(Replies, Parent);
false ->
collect_replies(Collect)
end.
-spec try_collect_all(#collect{}) -> _.
try_collect_all(Collect=#collect{reqid=ReqId}) ->
Timeout = riak_ensemble_config:notfound_read_delay(),
erlang:send_after(Timeout, self(), {try_collect_all_timeout, ReqId}),
try_collect_all_impl(Collect).
try_collect_all_impl(Collect=#collect{id=Id,
replies=Replies0,
parent={From, Ref},
reqid=ReqId,
views=Views}) ->
receive
{'$gen_all_state_event', Event} ->
{reply, ReqId, Peer, Reply} = Event,
Replies = [{Peer, Reply}|Replies0],
case quorum_met(Replies, Id, Views, all) of
true ->
%% At this point we should be guaranteed to have already
%% gotten a parent that we can reply to:
?OUT("Met quorum with Event ~p Replies ~p", [Event, Replies, Views]),
From ! {Ref, ok, Replies};
false ->
?OUT("Got additional message ~p but quorum still not met", [Event]),
try_collect_all(Collect#collect{replies=Replies});
nack ->
%% Since we're waiting for all, we may see a nack from even
%% just a single negative response. But, we already know we
%% have a quorum of positive replies, so we can still send
%% back an 'ok' response with the replies we've gotten.
?OUT("Got a nack! Returning replies so far: ~p", [Replies]),
From ! {Ref, ok, Replies}
end;
{try_collect_all_timeout, ReqId} ->
?OUT("Timed out waiting for try_collect_all", []),
From ! {Ref, ok, Replies0}
end.
-spec wait_for_quorum(future()) -> {quorum_met, [peer_reply()]} |
{timeout, [peer_reply()]}.
wait_for_quorum(undefined) ->
{quorum_met, []};
wait_for_quorum(Pid) ->
Ref = make_ref(),
Pid ! {waiting, self(), Ref},
receive
{Ref, ok, Replies} ->
{Valid, _Nacks} = find_valid(Replies),
{quorum_met, Valid};
{Ref, timeout, Replies} ->
{timeout, Replies}
end.
%%%===================================================================
-spec handle_reply(any(), peer_id(), any(), msg_state()) -> msg_state().
handle_reply(ReqId, Peer, Reply, MsgState=#msgstate{awaiting=Awaiting}) ->
case ReqId == Awaiting of
true ->
add_reply(Peer, Reply, MsgState);
false ->
MsgState
end.
-spec add_reply(peer_id(), any(), msg_state()) -> msg_state().
add_reply(Peer, Reply, MsgState=#msgstate{timer=Timer}) ->
Replies = [{Peer, Reply} | MsgState#msgstate.replies],
case quorum_met(Replies, MsgState) of
true ->
cancel_timer(Timer),
{Valid, _Nacks} = find_valid(Replies),
gen_fsm:send_event(self(), {quorum_met, Valid}),
MsgState#msgstate{replies=[], awaiting=undefined, timer=undefined};
false ->
MsgState#msgstate{replies=Replies};
nack ->
cancel_timer(Timer),
quorum_timeout(MsgState#msgstate{replies=Replies})
end.
-spec quorum_timeout(msg_state()) -> msg_state().
quorum_timeout(#msgstate{replies=Replies}) ->
{Valid, _Nacks} = find_valid(Replies),
gen_fsm:send_event(self(), {timeout, Valid}),
#msgstate{awaiting=undefined, timer=undefined, replies=[]}.
%%%===================================================================
-spec quorum_met([peer_reply()], msg_state()) -> true | false | nack.
quorum_met(Replies, #msgstate{id=Id, views=Views, required=Required}) ->
quorum_met(Replies, Id, Views, Required).
-spec quorum_met([peer_reply()], peer_id(), views(), required()) -> true | false | nack.
quorum_met(Replies, Id, Views, Required) ->
quorum_met(Replies, Id, Views, Required, undefined).
-spec quorum_met([peer_reply()], peer_id(),
views(), required(), extra_check()) -> true | false | nack.
quorum_met(Replies, _Id, [], _Required, Extra) ->
case Extra of
undefined ->
true;
_ ->
Extra(Replies)
end;
quorum_met(Replies, Id, [Members|Views], Required, Extra) ->
Filtered = [Reply || Reply={Peer,_} <- Replies,
lists:member(Peer, Members)],
{Valid, Nacks} = find_valid(Filtered),
Quorum = case Required of
quorum ->
length(Members) div 2 + 1;
all_or_quorum ->
length(Members) div 2 + 1;
other ->
length(Members) div 2 + 1;
all ->
length(Members)
end,
Heard = case (Required =/= other) andalso lists:member(Id, Members) of
true ->
length(Valid) + 1;
false ->
length(Valid)
end,
if Heard >= Quorum ->
?OUT("~p//~nM: ~p~nV: ~p~nN: ~p: view-met~n", [Id, Members, Valid, Nacks]),
quorum_met(Replies, Id, Views, Required, Extra);
length(Nacks) >= Quorum ->
?OUT("~p//~nM: ~p~nV: ~p~nN: ~p: nack~n", [Id, Members, Valid, Nacks]),
nack;
(Heard + length(Nacks)) =:= length(Members) ->
?OUT("~p//~nM: ~p~nV: ~p~nN: ~p: nack~n", [Id, Members, Valid, Nacks]),
nack;
true ->
?OUT("~p//~nM: ~p~nV: ~p~nN: ~p: false~n", [Id, Members, Valid, Nacks]),
false
end.
-spec find_valid([peer_reply()]) -> {[peer_reply()], [peer_nack()]}.
find_valid(Replies) ->
{Valid, Nacks} = lists:partition(fun({_, nack}) ->
false;
(_) ->
true
end, Replies),
{Valid, Nacks}.
%%%===================================================================
-spec make_request(msg()) -> {reqid(), tuple()}.
make_request(Msg) ->
ReqId = make_ref(),
From = make_from(self(), ReqId),
Request = if is_tuple(Msg) ->
erlang:append_element(Msg, From);
true ->
{Msg, From}
end,
{ReqId, Request}.
-spec make_from(pid(), reqid()) -> msg_from().
make_from(Pid, ReqId) ->
{riak_ensemble_msg, Pid, ReqId}.
%%%===================================================================
-spec send_after(timeout(), pid(), msg()) -> timer().
send_after(Time, Dest, Msg) ->
erlang:send_after(Time, Dest, Msg).
-spec cancel_timer(timer()) -> ok.
cancel_timer(Timer) ->
case erlang:cancel_timer(Timer) of
false ->
receive
quorum_timeout -> ok
after
0 -> ok
end;
_ ->
ok
end.