1#!/usr/bin/env escript
2%% -*- Mode: Erlang; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
3%%! -smp enable
4
5% Licensed under the Apache License, Version 2.0 (the "License"); you may not
6% use this file except in compliance with the License. You may obtain a copy of
7% the License at
8%
9%   http://www.apache.org/licenses/LICENSE-2.0
10%
11% Unless required by applicable law or agreed to in writing, software
12% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14% License for the specific language governing permissions and limitations under
15% the License.
16
17-include_lib("couch_set_view/include/couch_set_view.hrl").
18
19-define(JSON_ENCODE(V), ejson:encode(V)). % couch_db.hrl
20-define(MAX_WAIT_TIME, 900 * 1000).
21
22-define(etap_match(Got, Expected, Desc),
23        etap:fun_is(fun(XXXXXX) ->
24            case XXXXXX of Expected -> true; _ -> false end
25        end, Got, Desc)).
26
27test_set_name() -> <<"couch_test_set_unindexable_partitions">>.
28num_set_partitions() -> 64.
29ddoc_id() -> <<"_design/test">>.
30num_docs() -> 16448.  % keep it a multiple of num_set_partitions()
31
32
33main(_) ->
34    test_util:init_code_path(),
35
36    etap:plan(151),
37    case (catch test()) of
38        ok ->
39            etap:end_tests();
40        Other ->
41            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
42            etap:bail(Other)
43    end,
44    ok.
45
46
47test() ->
48    couch_set_view_test_util:start_server(test_set_name()),
49
50    create_set(),
51    ReplicaParts = lists:seq(num_set_partitions() div 2, num_set_partitions() - 1),
52    couch_set_view:add_replica_partitions(
53        mapreduce_view, test_set_name(), ddoc_id(), ReplicaParts),
54
55    ActiveParts = lists:seq(0, (num_set_partitions() div 2) - 1),
56    ValueGenFun1 = fun(I) -> I end,
57    update_documents(0, num_docs(), ValueGenFun1),
58
59    main_trigger_wait_for_initial_build(),
60    replica_trigger_wait_for_initial_build(),
61
62    ExpectedSeqs1 = couch_set_view_test_util:get_db_seqs(test_set_name(), ActiveParts),
63    ExpectedUnindexableSeqs1 = [],
64    verify_btrees_1(ActiveParts, [], ExpectedSeqs1, ExpectedUnindexableSeqs1, ValueGenFun1),
65
66    Unindexable = lists:seq(0, (num_set_partitions() div 2) - 1, 2),
67    etap:diag("Marking the following partitions as unindexable: ~w", [Unindexable]),
68
69    etap:is(
70        couch_set_view:mark_partitions_unindexable(
71            mapreduce_view, test_set_name(), ddoc_id(), Unindexable),
72        ok,
73        "Marked unindexable partitions"),
74
75    ExpectedSeqs2 = [
76        {P, S} ||
77        {P, S} <- couch_set_view_test_util:get_db_seqs(test_set_name(), ActiveParts),
78        (not ordsets:is_element(P, Unindexable))
79    ],
80    ExpectedUnindexableSeqs2 = [
81        {P, S} ||
82        {P, S} <- couch_set_view_test_util:get_db_seqs(test_set_name(), ActiveParts),
83        ordsets:is_element(P, Unindexable)
84    ],
85    verify_btrees_1(ActiveParts, [], ExpectedSeqs2, ExpectedUnindexableSeqs2, ValueGenFun1),
86
87    compact_view_group(),
88    verify_btrees_1(ActiveParts, [], ExpectedSeqs2, ExpectedUnindexableSeqs2, ValueGenFun1),
89
90    MarkResult1 = try
91        couch_set_view:set_partition_states(
92            mapreduce_view, test_set_name(), ddoc_id(), [],
93            [lists:last(Unindexable)], [])
94    catch throw:Error ->
95        Error
96    end,
97    ?etap_match(MarkResult1,
98                {error, _},
99                "Error setting unindexable partition to passive state"),
100
101    MarkResult2 = try
102        couch_set_view:set_partition_states(
103            mapreduce_view, test_set_name(), ddoc_id(), [], [],
104            [lists:last(Unindexable)])
105    catch throw:Error2 ->
106        Error2
107    end,
108    ?etap_match(MarkResult2,
109                {error, _},
110                "Error setting unindexable partition to cleanup state"),
111
112    MarkResult3 = try
113        couch_set_view:mark_partitions_unindexable(
114            mapreduce_view, test_set_name(), ddoc_id(),
115            [lists:last(ReplicaParts)])
116    catch throw:Error3 ->
117        Error3
118    end,
119    ?etap_match(MarkResult3,
120                {error, _},
121                "Error marking replica partition as unindexable"),
122
123    ValueGenFun2 = fun(I) -> I * 3 end,
124    update_documents(0, num_docs(), ValueGenFun2),
125    % stale=false request should block caller until all active partitions are
126    % marked as indexable again (updates to them happened after being marked as
127    % unindexable).
128    {ClientPid, ClientMonRef} = spawn_monitor(fun() ->
129        Snapshot = get_group_snapshot(false),
130        exit({ok, Snapshot})
131    end),
132    receive
133    {'DOWN', ClientMonRef, process, ClientPid, Reason2} ->
134        etap:bail("Client was not blocked, exit reason: " ++ couch_util:to_list(Reason2))
135    after 10000 ->
136        etap:diag("Client is blocked")
137    end,
138
139    ExpectedSeqs3 = [
140        {P, S} ||
141        {P, S} <- couch_set_view_test_util:get_db_seqs(test_set_name(), ActiveParts),
142        (not ordsets:is_element(P, Unindexable))
143    ],
144    ExpectedUnindexableSeqs3 = ExpectedUnindexableSeqs2,
145    verify_btrees_2(ExpectedSeqs3, ExpectedUnindexableSeqs3, ValueGenFun1, ValueGenFun2),
146
147    compact_view_group(),
148    verify_btrees_2(ExpectedSeqs3, ExpectedUnindexableSeqs3, ValueGenFun1, ValueGenFun2),
149
150    etap:diag("Marking the following partitions as indexable (again): ~w", [Unindexable]),
151
152    etap:is(
153        couch_set_view:mark_partitions_indexable(
154            mapreduce_view, test_set_name(), ddoc_id(), Unindexable),
155        ok,
156        "Marked indexable partitions"),
157
158    trigger_main_update_and_wait(),
159
160    % Client should have been unblocked already or is just about to be unblocked.
161    receive
162    {'DOWN', ClientMonRef, process, ClientPid, {ok, #set_view_group{}}} ->
163        etap:diag("Client was unblocked");
164    {'DOWN', ClientMonRef, process, ClientPid, Reason} ->
165        etap:bail("Client was not blocked, exit reason: " ++ couch_util:to_list(Reason))
166    after 10000 ->
167        etap:bail("Client is still blocked")
168    end,
169
170    ExpectedSeqs4 = couch_set_view_test_util:get_db_seqs(test_set_name(), ActiveParts),
171    ExpectedUnindexableSeqs4 = [],
172    verify_btrees_1(ActiveParts, [], ExpectedSeqs4, ExpectedUnindexableSeqs4, ValueGenFun2),
173
174    compact_view_group(),
175    verify_btrees_1(ActiveParts, [], ExpectedSeqs4, ExpectedUnindexableSeqs4, ValueGenFun2),
176
177    % Mark first replica partition as active. Verify that after this it's possible
178    % to mark it as unindexable and then back to indexable once again.
179    GroupPid = couch_set_view:get_group_pid(
180        mapreduce_view, test_set_name(), ddoc_id(), prod),
181    ok = gen_server:call(GroupPid, {set_auto_transfer_replicas, false}, infinity),
182
183    ActivateReplicaResult = couch_set_view:set_partition_states(
184        mapreduce_view, test_set_name(), ddoc_id(),
185        [hd(ReplicaParts)], [], []),
186    ?etap_match(ActivateReplicaResult,
187                ok,
188                "Activated replica partition " ++ integer_to_list(hd(ReplicaParts))),
189
190    Unindexable2 = [hd(ReplicaParts)],
191    etap:is(
192        couch_set_view:mark_partitions_unindexable(
193            mapreduce_view, test_set_name(), ddoc_id(), Unindexable2),
194        ok,
195        "Marked replica partition on transfer as unindexable"),
196
197    PassiveParts = [hd(ReplicaParts)],
198    ExpectedSeqs5 = ExpectedSeqs4,
199    ExpectedUnindexableSeqs5 = [{hd(ReplicaParts), 0}],
200    verify_btrees_1(ActiveParts, PassiveParts, ExpectedSeqs5, ExpectedUnindexableSeqs5, ValueGenFun2),
201    compact_view_group(),
202    verify_btrees_1(ActiveParts, PassiveParts, ExpectedSeqs5, ExpectedUnindexableSeqs5, ValueGenFun2),
203
204    etap:is(
205        couch_set_view:mark_partitions_indexable(
206            mapreduce_view, test_set_name(), ddoc_id(), Unindexable2),
207        ok,
208        "Marked replica partition on transfer as indexable again"),
209
210    ExpectedSeqs6 = ExpectedSeqs5 ++ [{hd(ReplicaParts), 0}],
211    ExpectedUnindexableSeqs6 = [],
212    verify_btrees_1(ActiveParts, PassiveParts, ExpectedSeqs6, ExpectedUnindexableSeqs6, ValueGenFun2),
213    compact_view_group(),
214    verify_btrees_1(ActiveParts, PassiveParts, ExpectedSeqs6, ExpectedUnindexableSeqs6, ValueGenFun2),
215
216    % Test for MB-8677
217    etap:is(
218        couch_set_view:mark_partitions_unindexable(mapreduce_view, test_set_name(), ddoc_id(), Unindexable2),
219        ok,
220        "Marked replica partition on transfer as unindexable again"),
221    etap:is(
222        couch_set_view:set_partition_states(mapreduce_view, test_set_name(), ddoc_id(), Unindexable2, [], []),
223        ok,
224        "Request to mark replicas on transfer to active didn't raise an error"),
225    % Trigger the automatic transfer
226    ok = gen_server:call(GroupPid, {set_auto_transfer_replicas, true}, infinity),
227    trigger_main_update_and_wait(),
228    ExpectedSeqs7 = ExpectedSeqs5 ++ [{hd(ReplicaParts), 514}],
229    verify_btrees_1(ActiveParts ++ Unindexable2,
230                    [],
231                    ExpectedSeqs7, ExpectedUnindexableSeqs5, ValueGenFun2),
232
233    % Mark it as indexable again, so that it can be cleanup
234    etap:is(
235        couch_set_view:mark_partitions_indexable(mapreduce_view, test_set_name(), ddoc_id(), Unindexable2),
236        ok,
237        "Marked replica partition on transfer as indexable again"),
238
239    ok = couch_set_view:set_partition_states(
240        mapreduce_view, test_set_name(), ddoc_id(), [], [], [hd(ReplicaParts)]),
241    wait_for_main_cleanup(),
242
243    verify_btrees_1(ActiveParts, [], ExpectedSeqs4, ExpectedUnindexableSeqs4, ValueGenFun2),
244    compact_view_group(),
245    verify_btrees_1(ActiveParts, [], ExpectedSeqs4, ExpectedUnindexableSeqs4, ValueGenFun2),
246
247    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
248    couch_set_view_test_util:stop_server(),
249    ok.
250
251
252get_group_snapshot(Staleness) ->
253    GroupPid = couch_set_view:get_group_pid(
254        mapreduce_view, test_set_name(), ddoc_id(), prod),
255    {ok, Group, _} = gen_server:call(
256        GroupPid, #set_view_group_req{stale = Staleness}, infinity),
257    Group.
258
259
260trigger_main_update_and_wait() ->
261    GroupPid = couch_set_view:get_group_pid(
262        mapreduce_view, test_set_name(), ddoc_id(), prod),
263    trigger_update_and_wait(GroupPid).
264
265trigger_replica_update_and_wait() ->
266    GroupPid = couch_set_view:get_group_pid(
267        mapreduce_view, test_set_name(), ddoc_id(), prod),
268    {ok, ReplicaGroupPid} = gen_server:call(GroupPid, replica_pid, infinity),
269    trigger_update_and_wait(ReplicaGroupPid).
270
271trigger_update_and_wait(GroupPid) ->
272    etap:diag("Trigerring index update"),
273    {ok, UpPid} = gen_server:call(GroupPid, {start_updater, []}, infinity),
274    case is_pid(UpPid) of
275    true ->
276        ok;
277    false ->
278        etap:bail("Updater was not triggered~n")
279    end,
280    Ref = erlang:monitor(process, UpPid),
281    receive
282    {'DOWN', Ref, process, UpPid, {updater_finished, _}} ->
283        ok;
284    {'DOWN', Ref, process, UpPid, noproc} ->
285        ok;
286    {'DOWN', Ref, process, UpPid, Reason} ->
287        etap:bail("Failure updating main group: " ++ couch_util:to_list(Reason))
288    after ?MAX_WAIT_TIME ->
289        etap:bail("Timeout waiting for main group update")
290    end.
291
292
293wait_for_main_cleanup() ->
294    GroupPid = couch_set_view:get_group_pid(
295        mapreduce_view, test_set_name(), ddoc_id(), prod),
296    {ok, CleanerPid} = gen_server:call(GroupPid, start_cleaner, infinity),
297    CleanerRef = erlang:monitor(process, CleanerPid),
298    receive
299    {'DOWN', CleanerRef, _, _, _} ->
300        ok
301    after 60000 ->
302        etap:bail("Timeout waiting for cleaner to finish")
303    end.
304
305
306create_set() ->
307    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
308    couch_set_view_test_util:create_set_dbs(test_set_name(), num_set_partitions()),
309    couch_set_view:cleanup_index_files(mapreduce_view, test_set_name()),
310    etap:diag("Creating the set databases (# of partitions: " ++
311        integer_to_list(num_set_partitions()) ++ ")"),
312    DDoc = {[
313        {<<"meta">>, {[{<<"id">>, ddoc_id()}]}},
314        {<<"json">>, {[
315            {<<"language">>, <<"javascript">>},
316            {<<"views">>, {[
317                {<<"view_1">>, {[
318                    {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value); }">>},
319                    {<<"reduce">>, <<"_sum">>}
320                ]}}
321            ]}}
322        ]}}
323    ]},
324    ok = couch_set_view_test_util:update_ddoc(test_set_name(), DDoc),
325    etap:diag("Configuring set view with partitions [0 .. 31] as active"),
326    Params = #set_view_params{
327        max_partitions = num_set_partitions(),
328        active_partitions = lists:seq(0, (num_set_partitions() div 2) - 1),
329        passive_partitions = [],
330        use_replica_index = true
331    },
332    ok = couch_set_view:define_group(
333        mapreduce_view, test_set_name(), ddoc_id(), Params).
334
335
336update_documents(StartId, Count, ValueGenFun) ->
337    etap:diag("Updating " ++ integer_to_list(Count) ++ " new documents"),
338    DocList0 = lists:map(
339        fun(I) ->
340            {I rem num_set_partitions(), {[
341                {<<"meta">>, {[{<<"id">>, doc_id(I)}]}},
342                {<<"json">>, {[
343                    {<<"value">>, ValueGenFun(I)}
344                ]}}
345            ]}}
346        end,
347        lists:seq(StartId, StartId + Count - 1)),
348    DocList = [Doc || {_, Doc} <- lists:keysort(1, DocList0)],
349    ok = couch_set_view_test_util:populate_set_sequentially(
350        test_set_name(),
351        lists:seq(0, num_set_partitions() - 1),
352        DocList).
353
354
355doc_id(I) ->
356    iolist_to_binary(io_lib:format("doc_~8..0b", [I])).
357
358
359compact_view_group() ->
360    {ok, CompactPid} = couch_set_view_compactor:start_compact(
361        mapreduce_view, test_set_name(), ddoc_id(), main),
362    Ref = erlang:monitor(process, CompactPid),
363    etap:diag("Waiting for view group compaction to finish"),
364    receive
365    {'DOWN', Ref, process, CompactPid, normal} ->
366        ok;
367    {'DOWN', Ref, process, CompactPid, noproc} ->
368        ok;
369    {'DOWN', Ref, process, CompactPid, Reason} ->
370        etap:bail("Failure compacting main group: " ++ couch_util:to_list(Reason))
371    after ?MAX_WAIT_TIME ->
372        etap:bail("Timeout waiting for main group compaction to finish")
373    end.
374
375
376verify_btrees_1(ActiveParts, PassiveParts, ExpectedSeqs,
377                ExpectedUnindexableSeqs, ValueGenFun) ->
378    Group = get_group_snapshot(ok),
379    #set_view_group{
380        id_btree = IdBtree,
381        views = Views,
382        index_header = #set_view_index_header{
383            seqs = HeaderUpdateSeqs,
384            abitmask = Abitmask,
385            pbitmask = Pbitmask,
386            cbitmask = Cbitmask,
387            unindexable_seqs = UnindexableSeqs
388        }
389    } = Group,
390    etap:is(1, length(Views), "1 view btree in the group"),
391    [View1] = Views,
392    #set_view{
393        indexer = #mapreduce_view{
394            btree = View1Btree
395        }
396    } = View1,
397    AllSeqs = HeaderUpdateSeqs ++ UnindexableSeqs,
398    ExpectedActiveBitmask = couch_set_view_util:build_bitmask(ActiveParts),
399    ExpectedPassiveBitmask = couch_set_view_util:build_bitmask(PassiveParts),
400    ExpectedIndexedBitmask = couch_set_view_util:build_bitmask(
401        [P || P <- (ActiveParts ++ PassiveParts), couch_util:get_value(P, AllSeqs, -1) > 0]),
402    ExpectedKVCount = (num_docs() div num_set_partitions()) *
403        length([P || P <- ActiveParts, couch_util:get_value(P, AllSeqs, -1) > 0]),
404    ExpectedView1Reduction = lists:sum([
405        ValueGenFun(I) || I <- lists:seq(0, num_docs() - 1),
406            lists:member(I rem num_set_partitions(), ActiveParts),
407            couch_util:get_value(I rem num_set_partitions(), AllSeqs, -1) > 0
408    ]),
409
410    etap:is(
411        couch_set_view_test_util:full_reduce_id_btree(Group, IdBtree),
412        {ok, {ExpectedKVCount, ExpectedIndexedBitmask}},
413        "Id Btree has the right reduce value"),
414    etap:is(
415        couch_set_view_test_util:full_reduce_view_btree(Group, View1Btree),
416        {ok, {ExpectedKVCount, [ExpectedView1Reduction], ExpectedIndexedBitmask}},
417        "View1 Btree has the right reduce value 1"),
418
419    etap:is(HeaderUpdateSeqs, ExpectedSeqs, "Header has right update seqs list"),
420    etap:is(UnindexableSeqs, ExpectedUnindexableSeqs, "Header has right unindexable seqs list"),
421    etap:is(Abitmask, ExpectedActiveBitmask, "Header has right active bitmask"),
422    etap:is(Pbitmask, ExpectedPassiveBitmask, "Header has right passive bitmask"),
423    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
424
425    etap:diag("Verifying the Id Btree"),
426    MaxPerPart = num_docs() div num_set_partitions(),
427    {ok, _, {_, _, _, IdBtreeFoldResult}} = couch_set_view_test_util:fold_id_btree(
428        Group,
429        IdBtree,
430        fun(Kv, _, {P0, I0, C0, It}) ->
431            case C0 >= MaxPerPart of
432            true ->
433                P = P0 + 1,
434                I = P,
435                C = 1;
436            false ->
437                P = P0,
438                I = I0,
439                C = C0 + 1
440            end,
441            true = (P < num_set_partitions()),
442            UserKey = DocId = doc_id(I),
443            Value = [{View1#set_view.id_num, UserKey}],
444            ExpectedKv = {<<P:16, DocId/binary>>, {P, Value}},
445            case ExpectedKv =:= Kv of
446            true ->
447                ok;
448            false ->
449                etap:bail("Id Btree has an unexpected KV at iteration " ++ integer_to_list(It))
450            end,
451            {ok, {P, I + num_set_partitions(), C, It + 1}}
452        end,
453        {0, 0, 0, 0}, []),
454    etap:is(IdBtreeFoldResult, ExpectedKVCount,
455        "Id Btree has " ++ integer_to_list(ExpectedKVCount) ++ " entries"),
456
457    etap:diag("Verifying the View1 Btree"),
458    {ok, _, {_, View1BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
459        Group,
460        View1Btree,
461        fun(Kv, _, {NextId, I}) ->
462            PartId = NextId rem num_set_partitions(),
463            Key = DocId = doc_id(NextId),
464            Value = ValueGenFun(NextId),
465            ExpectedKv = {{Key, DocId}, {PartId, Value}},
466            case ExpectedKv =:= Kv of
467            true ->
468                ok;
469            false ->
470                etap:bail("View1 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
471            end,
472
473            HighestActivePartId = length(ActiveParts) - 1,
474            case PartId =:= HighestActivePartId of
475            true ->
476                NumNonActiveParts = num_set_partitions() - HighestActivePartId,
477                {ok, {NextId + NumNonActiveParts, I + 1}};
478            false ->
479                {ok, {NextId + 1, I + 1}}
480            end
481        end,
482        {0, 0}, []),
483    etap:is(View1BtreeFoldResult, ExpectedKVCount,
484        "View1 Btree has " ++ integer_to_list(ExpectedKVCount) ++ " entries"),
485    ok.
486
487
488verify_btrees_2(ExpectedSeqs, ExpectedUnindexableSeqs, ValueGenFun1, ValueGenFun2) ->
489    Group = get_group_snapshot(ok),
490    #set_view_group{
491        id_btree = IdBtree,
492        views = Views,
493        index_header = #set_view_index_header{
494            seqs = HeaderUpdateSeqs,
495            abitmask = Abitmask,
496            pbitmask = Pbitmask,
497            cbitmask = Cbitmask,
498            unindexable_seqs = UnindexableSeqs
499        }
500    } = Group,
501    etap:is(1, length(Views), "1 view btree in the group"),
502    [View1] = Views,
503    #set_view{
504        indexer = #mapreduce_view{
505            btree = View1Btree
506        }
507    } = View1,
508    ActiveParts = lists:seq(0, (num_set_partitions() div 2) - 1),
509    ExpectedBitmask = couch_set_view_util:build_bitmask(ActiveParts),
510    ExpectedKVCount = (num_docs() div num_set_partitions()) * length(ActiveParts),
511    ExpectedView1Reduction = lists:sum([
512        ValueGenFun1(I) || I <- lists:seq(0, num_docs() - 1),
513        orddict:is_key(I rem num_set_partitions(), ExpectedUnindexableSeqs),
514        lists:member(I rem num_set_partitions(), ActiveParts)
515    ]) + lists:sum([
516        ValueGenFun2(I) || I <- lists:seq(0, num_docs() - 1),
517        (not orddict:is_key(I rem num_set_partitions(), ExpectedUnindexableSeqs)),
518        lists:member(I rem num_set_partitions(), ActiveParts)
519    ]),
520
521    etap:is(
522        couch_set_view_test_util:full_reduce_id_btree(Group, IdBtree),
523        {ok, {ExpectedKVCount, ExpectedBitmask}},
524        "Id Btree has the right reduce value"),
525    etap:is(
526        couch_set_view_test_util:full_reduce_view_btree(Group, View1Btree),
527        {ok, {ExpectedKVCount, [ExpectedView1Reduction], ExpectedBitmask}},
528        "View1 Btree has the right reduce value 2"),
529
530    etap:is(HeaderUpdateSeqs, ExpectedSeqs, "Header has right update seqs list"),
531    etap:is(UnindexableSeqs, ExpectedUnindexableSeqs, "Header has right unindexable seqs list"),
532    etap:is(Abitmask, ExpectedBitmask, "Header has right active bitmask"),
533    etap:is(Pbitmask, 0, "Header has right passive bitmask"),
534    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
535
536    etap:diag("Verifying the Id Btree"),
537    MaxPerPart = num_docs() div num_set_partitions(),
538    {ok, _, {_, _, _, IdBtreeFoldResult}} = couch_set_view_test_util:fold_id_btree(
539        Group,
540        IdBtree,
541        fun(Kv, _, {P0, I0, C0, It}) ->
542            case C0 >= MaxPerPart of
543            true ->
544                P = P0 + 1,
545                I = P,
546                C = 1;
547            false ->
548                P = P0,
549                I = I0,
550                C = C0 + 1
551            end,
552            true = (P < num_set_partitions()),
553            UserKey = DocId = doc_id(I),
554            Value = [{View1#set_view.id_num, UserKey}],
555            ExpectedKv = {<<P:16, DocId/binary>>, {P, Value}},
556            case ExpectedKv =:= Kv of
557            true ->
558                ok;
559            false ->
560                etap:bail("Id Btree has an unexpected KV at iteration " ++ integer_to_list(It))
561            end,
562            {ok, {P, I + num_set_partitions(), C, It + 1}}
563        end,
564        {0, 0, 0, 0}, []),
565    etap:is(IdBtreeFoldResult, ExpectedKVCount,
566        "Id Btree has " ++ integer_to_list(ExpectedKVCount) ++ " entries"),
567
568    etap:diag("Verifying the View1 Btree"),
569    {ok, _, {_, View1BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
570        Group,
571        View1Btree,
572        fun(Kv, _, {NextId, I}) ->
573            PartId = NextId rem num_set_partitions(),
574            case orddict:is_key(PartId, ExpectedUnindexableSeqs) of
575            true ->
576                Value = ValueGenFun1(NextId);
577            false ->
578                Value = ValueGenFun2(NextId)
579            end,
580            Key = DocId = doc_id(NextId),
581            ExpectedKv = {{Key, DocId}, {PartId, Value}},
582            case ExpectedKv =:= Kv of
583            true ->
584                ok;
585            false ->
586                etap:bail("View1 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
587            end,
588            case PartId =:= 31 of
589            true ->
590                {ok, {NextId + 33, I + 1}};
591            false ->
592                {ok, {NextId + 1, I + 1}}
593            end
594        end,
595        {0, 0}, []),
596    etap:is(View1BtreeFoldResult, ExpectedKVCount,
597        "View1 Btree has " ++ integer_to_list(ExpectedKVCount) ++ " entries"),
598    ok.
599
600
601main_trigger_wait_for_initial_build() ->
602    GroupPid = couch_set_view:get_group_pid(
603        mapreduce_view, test_set_name(), ddoc_id(), prod),
604    {ok, _, _} = gen_server:call(
605        GroupPid, #set_view_group_req{stale = false, debug = true}, ?MAX_WAIT_TIME).
606
607
608replica_trigger_wait_for_initial_build() ->
609    GroupPid = couch_set_view:get_group_pid(
610        mapreduce_view, test_set_name(), ddoc_id(), prod),
611    {ok, ReplicaGroupPid} = gen_server:call(GroupPid, replica_pid, infinity),
612    {ok, _, _} = gen_server:call(
613        ReplicaGroupPid, #set_view_group_req{stale = false, debug = true}, ?MAX_WAIT_TIME).
614