Skip to content

Commit 96da687

Browse files
committed
Change quorum_min/3 to quorum_max/3
* Ensure rafter_config:quorum_max/3 supports even numbers of servers * Make the code a bit clearer hopefully * Add some comments
1 parent 29f11dd commit 96da687

File tree

3 files changed

+46
-35
lines changed

3 files changed

+46
-35
lines changed

src/rafter_config.erl

+40-29
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,38 @@
33
-include("rafter.hrl").
44

55
%% API
6-
-export([quorum/3, quorum_min/3, voters/1, voters/2, followers/2,
6+
-export([quorum/3, quorum_max/3, voters/1, voters/2, followers/2,
77
reconfig/2, allow_config/2, has_vote/2]).
88

99
%%====================================================================
1010
%% API
1111
%%====================================================================
1212

13-
-spec quorum_min(term(), #config{} | [], dict()) -> non_neg_integer().
14-
quorum_min(_Me, #config{state=blank}, _) ->
13+
-spec quorum_max(peer(), #config{} | [], dict()) -> non_neg_integer().
14+
quorum_max(_Me, #config{state=blank}, _) ->
1515
0;
16-
quorum_min(Me, #config{state=stable, oldservers=OldServers}, Responses) ->
17-
quorum_min(Me, OldServers, Responses);
18-
quorum_min(Me, #config{state=staging, oldservers=OldServers}, Responses) ->
19-
quorum_min(Me, OldServers, Responses);
20-
quorum_min(Me, #config{state=transitional,
16+
quorum_max(Me, #config{state=stable, oldservers=OldServers}, Responses) ->
17+
quorum_max(Me, OldServers, Responses);
18+
quorum_max(Me, #config{state=staging, oldservers=OldServers}, Responses) ->
19+
quorum_max(Me, OldServers, Responses);
20+
quorum_max(Me, #config{state=transitional,
2121
oldservers=Old,
2222
newservers=New}, Responses) ->
23-
min(quorum_min(Me, Old, Responses), quorum_min(Me, New, Responses));
23+
min(quorum_max(Me, Old, Responses), quorum_max(Me, New, Responses));
2424

25-
%% Responses doesn't contain the local response so it will be marked as 0
26-
%% when it's a member of the consensus group. In this case we must
27-
%% skip past it in a sorted list so we add 1 to the quorum offset.
28-
quorum_min(_, [], _) ->
25+
%% Sort the values received from the peers from lowest to highest
26+
%% Peers that haven't responded have a 0 for their value.
27+
%% This peer (Me) will always have the maximum value
28+
quorum_max(_, [], _) ->
2929
0;
30-
quorum_min(Me, Servers, Responses) ->
31-
Indexes = lists:sort(lists:map(fun(S) -> index(S, Responses) end, Servers)),
32-
case lists:member(Me, Servers) of
33-
true ->
34-
lists:nth(length(Indexes) div 2 + 2, Indexes);
35-
false ->
36-
lists:nth(length(Indexes) div 2 + 1, Indexes)
37-
end.
30+
quorum_max(Me, Servers, Responses) when (length(Servers) rem 2) =:= 0->
31+
Values = sorted_values(Me, Servers, Responses),
32+
lists:nth(length(Values) div 2, Values);
33+
quorum_max(Me, Servers, Responses) ->
34+
Values = sorted_values(Me, Servers, Responses),
35+
lists:nth(length(Values) div 2 + 1, Values).
3836

39-
-spec quorum(term(), #config{} | list(), dict()) -> boolean().
37+
-spec quorum(peer(), #config{} | list(), dict()) -> boolean().
4038
quorum(_Me, #config{state=blank}, _Responses) ->
4139
false;
4240
quorum(Me, #config{state=stable, oldservers=OldServers}, Responses) ->
@@ -63,7 +61,7 @@ quorum(Me, Servers, Responses) ->
6361
end.
6462

6563
%% @doc list of voters excluding me
66-
-spec voters(term(), #config{}) -> list().
64+
-spec voters(peer(), #config{}) -> list().
6765
voters(Me, Config) ->
6866
lists:delete(Me, voters(Config)).
6967

@@ -74,7 +72,7 @@ voters(#config{state=transitional, oldservers=Old, newservers=New}) ->
7472
voters(#config{oldservers=Old}) ->
7573
Old.
7674

77-
-spec has_vote(term(), #config{}) -> boolean().
75+
-spec has_vote(peer(), #config{}) -> boolean().
7876
has_vote(_Me, #config{state=blank}) ->
7977
false;
8078
has_vote(Me, #config{state=transitional, oldservers=Old, newservers=New})->
@@ -83,7 +81,7 @@ has_vote(Me, #config{oldservers=Old}) ->
8381
lists:member(Me, Old).
8482

8583
%% @doc All followers. In staging, some followers are not voters.
86-
-spec followers(term(), #config{}) -> list().
84+
-spec followers(peer(), #config{}) -> list().
8785
followers(Me, #config{state=transitional, oldservers=Old, newservers=New}) ->
8886
lists:delete(Me, sets:to_list(sets:from_list(Old ++ New)));
8987
followers(Me, #config{state=staging, oldservers=Old, newservers=New}) ->
@@ -114,11 +112,24 @@ allow_config(_Config, _NewServers) ->
114112
%% Internal Functions
115113
%%====================================================================
116114

117-
-spec index(atom() | {atom(), atom()}, dict()) -> non_neg_integer().
118-
index(Peer, Responses) ->
115+
-spec sorted_values(peer(), [peer()], dict()) -> [non_neg_integer()].
116+
sorted_values(Me, Servers, Responses) ->
117+
Vals = lists:sort(lists:map(fun(S) -> value(S, Responses) end, Servers)),
118+
case lists:member(Me, Servers) of
119+
true ->
120+
%% Me is always in front because it is 0 from having no response
121+
%% Strip it off the front, and add the max to the end of the list
122+
[_ | T] = Vals,
123+
lists:reverse([lists:max(Vals) | lists:reverse(T)]);
124+
false ->
125+
Vals
126+
end.
127+
128+
-spec value(peer(), dict()) -> non_neg_integer().
129+
value(Peer, Responses) ->
119130
case dict:find(Peer, Responses) of
120-
{ok, Index} ->
121-
Index;
131+
{ok, Value} ->
132+
Value;
122133
error ->
123134
0
124135
end.

src/rafter_consensus_fsm.erl

+2-2
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ maybe_send_client_reply(_, _, State, _) ->
658658
maybe_send_read_replies(#state{me=Me,
659659
config=Config,
660660
send_clock_responses=Responses}=State0) ->
661-
Clock = rafter_config:quorum_min(Me, Config, Responses),
661+
Clock = rafter_config:quorum_max(Me, Config, Responses),
662662
{ok, Requests, State} = find_eligible_read_requests(Clock, State0),
663663
NewState = send_client_read_replies(Requests, State),
664664
NewState.
@@ -697,7 +697,7 @@ maybe_commit(#state{me=Me,
697697
commit_index=CommitIndex,
698698
config=Config,
699699
responses=Responses}=State) ->
700-
Min = rafter_config:quorum_min(Me, Config, Responses),
700+
Min = rafter_config:quorum_max(Me, Config, Responses),
701701
case Min > CommitIndex andalso safe_to_commit(Min, State) of
702702
true ->
703703
NewState = commit_entries(Min, State),

test/rafter_config_eqc.erl

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ eqc_test_() ->
4747
?_assertEqual(true,
4848
eqc:quickcheck(
4949
?QC_OUT(eqc:numtests(50, eqc:conjunction(
50-
[{prop_quorum_min,
51-
prop_quorum_min()},
50+
[{prop_quorum_max,
51+
prop_quorum_max()},
5252
{prop_config,
5353
prop_config()}])))))}
5454
]
@@ -68,11 +68,11 @@ cleanup(_) ->
6868
%% EQC Properties
6969
%% ====================================================================
7070

71-
prop_quorum_min() ->
71+
prop_quorum_max() ->
7272
?FORALL({Config, {Me, Responses}}, {config(), responses()},
7373
begin
7474
ResponsesDict = dict:from_list(Responses),
75-
case rafter_config:quorum_min(Me, Config, ResponsesDict) of
75+
case rafter_config:quorum_max(Me, Config, ResponsesDict) of
7676
0 ->
7777
true;
7878
QuorumMin ->

0 commit comments

Comments
 (0)