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" ).
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 ,
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 ().
185261redo () -> 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 .
195332maybe_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-
232367test_1inNreapfun (N ) ->
233368 fun (ReapRef , _Bool ) ->
234369 case erlang :phash2 ({ReapRef , os :timestamp ()}) rem N of
@@ -240,6 +375,31 @@ test_1inNreapfun(N) ->
240375test_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
244404standard_reaper_test_ () ->
245405 {timeout , 30 , fun standard_reaper_tester /0 }.
0 commit comments