Skip to content

Commit f951d39

Browse files
authored
Reap on all available primaries (#57)
* Reap on all available primaries Defer the reap only for unavailable primaries. Previously the reap was deferred on all primaries if some were not available. This caused issues with multi-cluster environments - where a set of reaps would occur on one cluster, but not on another (due to a node being unavailable). This caused full-sync reconciliation issues and the resurrection of tombstones. * Update comments after review * Clarify function name * Typo fix
1 parent 040104b commit f951d39

2 files changed

Lines changed: 204 additions & 35 deletions

File tree

src/riak_kv_queue_manager.erl

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,20 @@
3636
code_change/3,
3737
format_status/2]).
3838

39-
-export([start_link/2,
40-
start_job/3,
41-
request/2,
42-
bulk_request/2,
43-
stats/1,
44-
immediate_action/2,
45-
override_redo/2,
46-
clear_queue/1,
47-
stop_job/1]).
39+
-export(
40+
[
41+
start_link/2,
42+
start_job/3,
43+
request/2,
44+
redo_request/2,
45+
bulk_request/2,
46+
stats/1,
47+
immediate_action/2,
48+
override_redo/2,
49+
clear_queue/1,
50+
stop_job/1
51+
]
52+
).
4853

4954
-define(REDO_PRIORITY, 1).
5055
-define(REQUEST_PRIORITY, 2).
@@ -122,6 +127,10 @@ start_job(JobID, Module, RootPath) ->
122127
request(Pid, Reference) ->
123128
gen_server:cast(Pid, {request, Reference, ?REQUEST_PRIORITY}).
124129

130+
-spec redo_request(pid()|module(), term()) -> ok.
131+
redo_request(Pid, Reference) ->
132+
gen_server:cast(Pid, {request, Reference, ?REDO_PRIORITY}).
133+
125134
-spec bulk_request(pid()|module(), list()) -> ok.
126135
bulk_request(Pid, RefList) ->
127136
gen_server:call(Pid, {bulk_request, RefList, ?REQUEST_PRIORITY}, infinity).

src/riak_kv_reaper.erl

Lines changed: 186 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
%% additional reap capacity be required, then reap jobs could start their own
2727
%% reapers.
2828

29-
3029
-module(riak_kv_reaper).
3130
-ifdef(TEST).
3231
-include_lib("eunit/include/eunit.hrl").
@@ -38,12 +37,14 @@
3837
-define(QUEUE_LIMIT, 100000).
3938
-define(OVERFLOW_LIMIT, 10000000).
4039
-define(REDO_TIMEOUT, 2000).
41-
-define(OVERLOAD_PAUSE_MS, 10000).
4240
-define(TOMB_PAUSE, 2).
4341
% Used as flow control in the reaper, shared configuration with the delete
4442
% process where the pause has a dual-purpose, for both flow control and for
45-
% improving the probability that tombstone PUTs are propogated before a
43+
% improving the probability that tombstone PUTs are propagated before a
4644
% reap attempt is prompted
45+
-define(BUSY_FACTOR, 5).
46+
% Multiply the tombstone pause by this factor when there is a soft overload
47+
% state.
4748

4849
-export([start_link/0,
4950
start_job/1,
@@ -62,13 +63,26 @@
6263
get_limits/0,
6364
redo/0]).
6465

65-
-type reap_reference() ::
66+
-type index() :: chash:index_as_int().
67+
-type reap_reference() :: full_reap_request()|partial_reap_request().
68+
69+
-type full_reap_request() ::
6670
{{riak_object:bucket(), riak_object:key()}, vclock:vclock(), boolean()}.
6771
%% the reap reference is {Bucket, Key, Clock (of tombstone), Forward}. The
6872
%% Forward boolean() indicates if this reap should be replicated if
6973
%% riak_kv.repl_reap is true. When a reap is received via replication
7074
%% Forward should be set to false, to prevent reaps from perpetually
7175
%% circulating
76+
-type partial_reap_request() ::
77+
{{riak_object:bucket(), riak_object:key()}, non_neg_integer(), [index()]}.
78+
%% A request to reap from a specific index, eventually, when this index is
79+
%% up and available as a primary.
80+
%% Unlike a full reap request a partial reap request cannot be forwarded
81+
%% to another cluster.
82+
%% The DeleteHash is used rather than the tombstone clock to allow
83+
%% for the potential for downstream vnodes to prompt a partial reap request
84+
%% (the downstream vnode will only see the DeleteHash, not the clock in the
85+
%% reap request).
7286
-type job_id() :: pos_integer().
7387

7488
-export_type([reap_reference/0, job_id/0]).
@@ -150,37 +164,99 @@ get_limits() ->
150164
{RedoTimeout, QueueLimit, OverflowLimit}.
151165

152166
%% @doc
153-
%% If all primaries are up try and reap the tombstone. The reap may fail, but
154-
%% we will not redo - redo is only to handle the failure related to unavailable
155-
%% primaries
167+
%% Try and reap from as many primaries as are available - and for those not
168+
%% available defer, and retry until they are available.
169+
%% The initial message to check all preflists must prompt the replication of
170+
%% the reap unless that message is to be redone. If there is some partial reap
171+
%% then redo should not be used - instead a message with the outstanding
172+
%% indices is generated instead
156173
-spec action(reap_reference(), boolean()) -> boolean().
157-
action({{Bucket, Key}, TombClock, ToRepl}, Redo) ->
158-
BucketProps = riak_core_bucket:get_bucket(Bucket),
159-
DocIdx = riak_core_util:chash_key({Bucket, Key}, BucketProps),
160-
{n_val, N} = lists:keyfind(n_val, 1, BucketProps),
161-
PrefList = riak_core_apl:get_primary_apl(DocIdx, N, riak_kv),
162-
TombPause = app_helper:get_env(riak_kv, tombstone_pause, ?TOMB_PAUSE),
163-
case length(PrefList) of
164-
N ->
165-
PL0 = lists:map(fun({Target, primary}) -> Target end, PrefList),
166-
case check_all_mailboxes(PL0) of
174+
action({{_Bucket, _Key}, DeleteHash, []}, _Redo) when is_integer(DeleteHash) ->
175+
%% Nothing left to do
176+
true;
177+
action({{Bucket, Key}, DeleteHash, Indices}, Redo)
178+
when is_integer(DeleteHash), is_list(Indices) ->
179+
{TombPause, AplAnn} = setup_reap(Bucket, Key),
180+
case find_available(AplAnn, Indices) of
181+
{[], Deferred} when Deferred == Indices ->
182+
%% There are no indices available, so redo this message
183+
%% But pause first - so that the process is not in a tight loop
184+
%% re-checking
185+
timer:sleep(TombPause),
186+
respond_asfail_ifredo(Redo);
187+
{Available, Deferred} when Available =/= [] ->
188+
case check_all_mailboxes(Available) of
167189
ok ->
168190
riak_kv_vnode:reap(
169-
PL0,
191+
Available,
170192
{Bucket, Key},
171-
riak_object:delete_hash(TombClock)),
193+
DeleteHash
194+
),
195+
timer:sleep(TombPause),
196+
ok =
197+
riak_kv_queue_manager:redo_request(
198+
?MODULE,
199+
{{Bucket, Key}, DeleteHash, Deferred}
200+
),
201+
%% Some indices were available and the cluster is not
202+
%% stressed - so some updates made, and the remaining are
203+
%% deferred
204+
%% As an updated request has been sent to redo - this
205+
%% request must be marked as success (i.e. return `true`),
206+
%% to end the cycle of redo for this partially completed
207+
%% request.
208+
true;
209+
soft_loaded ->
210+
%% The cluster is busy - reaps need to slow down, so pause
211+
%% then requeue this message as-is.
212+
timer:sleep(TombPause * ?BUSY_FACTOR),
213+
respond_asfail_ifredo(Redo)
214+
end;
215+
{[], []} ->
216+
%% This may occur during shutdown - as no preflist can be accessed
217+
%% Just pass at this stage without pausing
218+
respond_asfail_ifredo(Redo)
219+
end;
220+
action({{Bucket, Key}, TombClock, ToRepl}, Redo)
221+
when is_boolean(ToRepl) ->
222+
{TombPause, AplAnn} = setup_reap(Bucket, Key),
223+
DeleteHash = riak_object:delete_hash(TombClock),
224+
case find_available(AplAnn, all) of
225+
{Available, []} ->
226+
case check_all_mailboxes(Available) of
227+
ok ->
228+
riak_kv_vnode:reap(
229+
Available,
230+
{Bucket, Key},
231+
DeleteHash
232+
),
172233
maybe_repl_reap(Bucket, Key, TombClock, ToRepl),
173234
timer:sleep(TombPause),
235+
%% All primaries were up, and the reap is replicated so
236+
%% indicate response as success (true)
174237
true;
175238
soft_loaded ->
176-
timer:sleep(?OVERLOAD_PAUSE_MS),
177-
if Redo -> false; true -> true end
178-
end;
239+
timer:sleep(TombPause * ?BUSY_FACTOR),
240+
%% The cluster is busy - reaps need to slow down, so pause
241+
%% then requeue this message as-is. Reap is not replicated
242+
%% yet as it has not been applied
243+
respond_asfail_ifredo(Redo)
244+
end;
179245
_ ->
180-
if Redo -> false; true -> true end
246+
maybe_repl_reap(Bucket, Key, TombClock, ToRepl),
247+
ok =
248+
riak_kv_queue_manager:request(
249+
?MODULE,
250+
{{Bucket, Key}, DeleteHash, indices_in_preflist(AplAnn)}
251+
),
252+
%% Not all primaries were available, so this reap request is
253+
%% re-queued but with all indices indicated as being required. The
254+
%% reap will now be replicated (as partial reaps never are) - so
255+
%% there may be a temporary discrepancy (hence why this is queued
256+
%% as an individual reap, which has higher priority than bulk reaps)
257+
true
181258
end.
182259

183-
184260
-spec redo() -> boolean().
185261
redo() -> true.
186262

@@ -190,6 +266,67 @@ redo() -> true.
190266

191267
-type preflist_entry() :: {non_neg_integer(), node()}.
192268

269+
-spec setup_reap(
270+
riak_object:bucket(), riak_object:key()) ->
271+
{non_neg_integer(), riak_core_apl:preflist_ann()}.
272+
setup_reap(Bucket, Key) ->
273+
TombPause = app_helper:get_env(riak_kv, tombstone_pause, ?TOMB_PAUSE),
274+
BucketProps = riak_core_bucket:get_bucket(Bucket),
275+
DocIdx = riak_core_util:chash_key({Bucket, Key}, BucketProps),
276+
{n_val, N} = lists:keyfind(n_val, 1, BucketProps),
277+
{
278+
TombPause,
279+
riak_core_apl:get_apl_ann(
280+
DocIdx,
281+
N,
282+
riak_core_node_watcher:nodes(riak_kv)
283+
)
284+
}.
285+
286+
%% @doc
287+
%% Redo by indicating the original request was not successful (false), but only
288+
%% if the request is marked as valid for redo (i.e. the Redo passed to the
289+
%% request is true). If the redo passed is false (redo not supported) - make a
290+
%% false claim of success (true), so as not to trigger Redo.
291+
-spec respond_asfail_ifredo(boolean()) -> boolean().
292+
respond_asfail_ifredo(Redo) ->
293+
not Redo.
294+
295+
%% @doc
296+
%% Find the available primaries (i.e. primaries on Up nodes) out of a sublist
297+
%% of required primaries. The `all` keyword is used instead of a sublist
298+
%% should the availability of all primaries in a preflist be the concern.
299+
%% Function returns a list of {Idx, Node} tuples for online primaries, and a
300+
%% list of Index integers for primaries not currently available.
301+
-spec find_available(
302+
riak_core_apl:preflist_ann(), all|list(index())) ->
303+
{list({index(), node()}), list(index())}.
304+
find_available(AplAnn, all) ->
305+
find_available(AplAnn, indices_in_preflist(AplAnn), [], []);
306+
find_available(AplAnn, Indices) ->
307+
find_available(AplAnn, Indices, [], []).
308+
309+
find_available([], _Reqd, Available, Deferred) ->
310+
{lists:reverse(Available), lists:reverse(Deferred)};
311+
find_available([{{Idx, Node}, primary}|Rest], Reqd, Available, Deferred) ->
312+
case lists:member(Idx, Reqd) of
313+
true ->
314+
find_available(Rest, Reqd, [{Idx, Node}|Available], Deferred);
315+
false ->
316+
find_available(Rest, Reqd, Available, Deferred)
317+
end;
318+
find_available([{{Idx, _Node}, fallback}|Rest], Reqd, Available, Deferred) ->
319+
case lists:member(Idx, Reqd) of
320+
true ->
321+
find_available(Rest, Reqd, Available, [Idx|Deferred]);
322+
false ->
323+
find_available(Rest, Reqd, Available, Deferred)
324+
end.
325+
326+
-spec indices_in_preflist(riak_core_apl:preflist_ann()) -> list(index()).
327+
indices_in_preflist(ApplAnn) ->
328+
lists:map(fun({{Idx, _N}, _T}) -> Idx end, ApplAnn).
329+
193330
-spec maybe_repl_reap(
194331
riak_object:bucket(), riak_object:key(), vclock:vclock(), boolean()) -> ok.
195332
maybe_repl_reap(Bucket, Key, TombClock, ToReap) ->
@@ -201,7 +338,6 @@ maybe_repl_reap(Bucket, Key, TombClock, ToReap) ->
201338
ok
202339
end.
203340

204-
205341
%% Protect against overloading the system when not reaping should any
206342
%% mailbox be in soft overload state
207343
-spec check_all_mailboxes(list(preflist_entry())) -> ok|soft_loaded.
@@ -228,7 +364,6 @@ check_mailbox({Idx, Node}) ->
228364

229365
-ifdef(TEST).
230366

231-
232367
test_1inNreapfun(N) ->
233368
fun(ReapRef, _Bool) ->
234369
case erlang:phash2({ReapRef, os:timestamp()}) rem N of
@@ -240,6 +375,31 @@ test_1inNreapfun(N) ->
240375
test_100reap(_ReapRef, _Bool) ->
241376
true.
242377

378+
find_available_test() ->
379+
N1 = 'node1@127.0.0.1',
380+
N2 = 'node2@127.0.0.1',
381+
N3 = 'node3@127.0.0.1',
382+
I1 = 0,
383+
I2 = 1 bsl 156,
384+
I3 = 2 bsl 156,
385+
AplAnn1 = [{{I1, N1}, primary}, {{I2, N2}, primary}, {{I3, N3}, primary}],
386+
?assertMatch(
387+
{[{I1, N1}, {I2, N2}, {I3, N3}], []},
388+
find_available(AplAnn1, all)
389+
),
390+
?assertMatch(
391+
{[{I1, N1}, {I3, N3}], []},
392+
find_available(AplAnn1, [I1, I3])
393+
),
394+
AplAnn2 = [{{I1, N1}, primary}, {{I2, N2}, primary}, {{I3, N3}, fallback}],
395+
?assertMatch(
396+
{[{I1, N1}, {I2, N2}], [I3]},
397+
find_available(AplAnn2, all)
398+
),
399+
?assertMatch(
400+
{[{I1, N1}], [I3]},
401+
find_available(AplAnn2, [I1, I3])
402+
).
243403

244404
standard_reaper_test_() ->
245405
{timeout, 30, fun standard_reaper_tester/0}.

0 commit comments

Comments
 (0)