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-define(JSON_ENCODE(V), ejson:encode(V)). % couch_db.hrl
18-define(JSON_DECODE(V), ejson:decode(V)). % couch_db.hrl
19-define(MAX_WAIT_TIME, 600 * 1000).
20
21-include_lib("couch_set_view/include/couch_set_view.hrl").
22
23% from couch_db.hrl
24-define(MIN_STR, <<>>).
25-define(MAX_STR, <<255>>).
26
27-record(view_query_args, {
28    start_key,
29    end_key,
30    start_docid = ?MIN_STR,
31    end_docid = ?MAX_STR,
32    direction = fwd,
33    inclusive_end = true,
34    limit = 10000000000,
35    skip = 0,
36    group_level = 0,
37    view_type = nil,
38    include_docs = false,
39    conflicts = false,
40    stale = false,
41    multi_get = false,
42    callback = nil,
43    list = nil,
44    run_reduce = true,
45    keys = nil,
46    view_name = nil,
47    debug = false,
48    filter = true,
49    type = main
50}).
51
52
53test_set_name() -> <<"couch_test_set_index_replicas_transfer">>.
54num_set_partitions() -> 64.
55ddoc_id() -> <<"_design/test">>.
56num_docs() -> 70848.  % keep it a multiple of num_set_partitions()
57
58
59main(_) ->
60    test_util:init_code_path(),
61
62    etap:plan(155),
63    case (catch test()) of
64        ok ->
65            etap:end_tests();
66        Other ->
67            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
68            etap:bail(Other)
69    end,
70    ok.
71
72
73test() ->
74    couch_set_view_test_util:start_server(test_set_name()),
75
76    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
77    couch_set_view_test_util:create_set_dbs(test_set_name(), num_set_partitions()),
78
79    create_set(),
80    add_documents(0, num_docs()),
81
82    MainGroupInfo1 = get_group_info(),
83    {RepGroupInfo1} = couch_util:get_value(replica_group_info, MainGroupInfo1),
84
85    ExpectedView1Result1 = num_docs() div 2,
86    ExpectedView2Result1 = lists:sum(
87        [I * 2 || I <- lists:seq(0, num_docs() - 1), (I rem 64) < 32]),
88
89    {View1QueryResult1, Group1} = query_reduce_view(<<"view_1">>, false),
90    {View2QueryResult1, Group2} = query_reduce_view(<<"view_2">>, false),
91    etap:is(
92        View1QueryResult1,
93        ExpectedView1Result1,
94        "Reduce view 1 has value " ++ couch_util:to_list(ExpectedView1Result1)),
95    etap:is(
96        View2QueryResult1,
97        ExpectedView2Result1,
98        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result1)),
99
100    verify_main_group_btrees_1(Group1),
101    verify_replica_group_btrees_1(Group1),
102    compare_groups(Group1, Group2),
103
104    etap:diag("Verifying main and replica group infos"),
105    etap:is(
106        couch_util:get_value(active_partitions, MainGroupInfo1),
107        lists:seq(0, 31),
108        "Main group has [ 0 .. 31 ] as active partitions"),
109    etap:is(
110        couch_util:get_value(passive_partitions, MainGroupInfo1),
111        [],
112        "Main group has [ ] as passive partitions"),
113    etap:is(
114        couch_util:get_value(cleanup_partitions, MainGroupInfo1),
115        [],
116        "Main group has [ ] as cleanup partitions"),
117    etap:is(
118        couch_util:get_value(replica_partitions, MainGroupInfo1),
119        [],
120        "Main group has [ ] as replica partitions"),
121    etap:is(
122        couch_util:get_value(replicas_on_transfer, MainGroupInfo1),
123        [],
124        "Main group has [ ] as replicas on transfer"),
125    etap:is(
126        couch_util:get_value(active_partitions, RepGroupInfo1),
127        [],
128        "Replica group has [ ] as active partitions"),
129    etap:is(
130        couch_util:get_value(passive_partitions, RepGroupInfo1),
131        [],
132        "Replica group has [ ] as passive partitions"),
133    etap:is(
134        couch_util:get_value(cleanup_partitions, RepGroupInfo1),
135        [],
136        "Replica group has [ ] as cleanup partitions"),
137
138    etap:diag("Marking partitions [ 32 .. 63 ] as replicas"),
139    ok = couch_set_view:add_replica_partitions(
140        mapreduce_view, test_set_name(), ddoc_id(), lists:seq(32, 63)),
141
142    MainGroupInfo2 = get_group_info(),
143    {RepGroupInfo2} = couch_util:get_value(replica_group_info, MainGroupInfo2),
144
145    etap:diag("Verifying main and replica group infos again"),
146    etap:is(
147        couch_util:get_value(active_partitions, MainGroupInfo2),
148        lists:seq(0, 31),
149        "Main group has [ 0 .. 31 ] as active partitions"),
150    etap:is(
151        couch_util:get_value(passive_partitions, MainGroupInfo2),
152        [],
153        "Main group has [ ] as passive partitions"),
154    etap:is(
155        couch_util:get_value(cleanup_partitions, MainGroupInfo2),
156        [],
157        "Main group has [ ] as cleanup partitions"),
158    etap:is(
159        couch_util:get_value(replica_partitions, MainGroupInfo2),
160        lists:seq(32, 63),
161        "Main group has [ 32 .. 63] as replica partitions"),
162    etap:is(
163        couch_util:get_value(replicas_on_transfer, MainGroupInfo2),
164        [],
165        "Main group has [ ] as replicas on transfer"),
166    etap:is(
167        couch_util:get_value(active_partitions, RepGroupInfo2),
168        [],
169        "Replica group has [ ] as active partitions"),
170    etap:is(
171        couch_util:get_value(passive_partitions, RepGroupInfo2),
172        lists:seq(32, 63),
173        "Replica group has [ 32 .. 63 ] as passive partitions"),
174    etap:is(
175        couch_util:get_value(cleanup_partitions, RepGroupInfo2),
176        [],
177        "Replica group has [ ] as cleanup partitions"),
178
179    {View1QueryResult2, _Group3} = query_reduce_view(<<"view_1">>, false),
180    {View2QueryResult2, _Group4} = query_reduce_view(<<"view_2">>, false),
181    etap:is(
182        View1QueryResult2,
183        ExpectedView1Result1,
184        "Reduce view 1 has value " ++ couch_util:to_list(ExpectedView1Result1)),
185    etap:is(
186        View2QueryResult2,
187        ExpectedView2Result1,
188        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result1)),
189
190    wait_for_replica_full_update(),
191
192    {View1QueryResult3, Group5} = query_reduce_view(<<"view_1">>, false),
193    {View2QueryResult3, Group6} = query_reduce_view(<<"view_2">>, false),
194    etap:is(
195        View1QueryResult3,
196        ExpectedView1Result1,
197        "Reduce view 1 has value " ++ couch_util:to_list(ExpectedView1Result1)),
198    etap:is(
199        View2QueryResult3,
200        ExpectedView2Result1,
201        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result1)),
202
203    verify_main_group_btrees_2(Group5),
204    verify_replica_group_btrees_2(Group5),
205    compare_groups(Group5, Group6),
206
207    MainDbSeqs = couch_set_view_test_util:get_db_seqs(
208        test_set_name(), lists:seq(0, 31)),
209    ReplicaDbSeqs = couch_set_view_test_util:get_db_seqs(
210        test_set_name(), lists:seq(32, 63)),
211    AllDbSeqs = ordsets:union(MainDbSeqs, ReplicaDbSeqs),
212    etap:is(
213        couch_set_view:get_indexed_seqs(mapreduce_view, test_set_name(), ddoc_id(), prod),
214        {ok, AllDbSeqs},
215        "couch_set_view:get_indexed_seqs/2 gave correct sequence numbers"),
216
217    ExpectedView1Result2 = num_docs(),
218    ExpectedView2Result2 = lists:sum([I * 2 || I <- lists:seq(0, num_docs() - 1)]),
219
220    etap:diag("Marking partitions [ 32 .. 63 ] as active"),
221    lists:foreach(
222        fun(I) ->
223            ok = couch_set_view:set_partition_states(
224                mapreduce_view, test_set_name(), ddoc_id(), [I], [], [])
225        end,
226        lists:seq(32, 63)),
227
228    MainGroupInfo3 = get_group_info(),
229    {RepGroupInfo3} = couch_util:get_value(replica_group_info, MainGroupInfo3),
230
231    {View1QueryResult4, _Group7} = query_reduce_view(<<"view_1">>, false),
232    {View2QueryResult4, _Group8} = query_reduce_view(<<"view_2">>, false),
233    etap:is(
234        View1QueryResult4,
235        ExpectedView1Result2,
236        "Reduce view 1 has value " ++ couch_util:to_list(ExpectedView1Result2)),
237    etap:is(
238        View2QueryResult4,
239        ExpectedView2Result2,
240        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result2)),
241
242    etap:diag("Waiting for transfer of replica partitions [ 32 .. 63 ] to main group"),
243    wait_for_main_full_update(MainGroupInfo2, ExpectedView1Result2, ExpectedView2Result2),
244    etap:diag("Replicas transferred to main group"),
245
246    verify_group_info_during_replicas_transfer(MainGroupInfo3, RepGroupInfo3),
247
248    wait_for_replica_cleanup(),
249
250    MainGroupInfo4 = get_group_info(),
251    {RepGroupInfo4} = couch_util:get_value(replica_group_info, MainGroupInfo4),
252    verify_group_info_after_replicas_transfer(MainGroupInfo4, RepGroupInfo4),
253
254    {View1QueryResult5, Group9}  = query_reduce_view(<<"view_1">>, false),
255    {View2QueryResult5, Group10} = query_reduce_view(<<"view_2">>, false),
256    etap:is(
257        View1QueryResult5,
258        ExpectedView1Result2,
259        "Reduce view has value " ++ couch_util:to_list(ExpectedView1Result2)),
260    etap:is(
261        View2QueryResult5,
262        ExpectedView2Result2,
263        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result2)),
264
265    verify_main_group_btrees_3(Group9),
266    verify_replica_group_btrees_3(Group9),
267    compare_groups(Group9, Group10),
268
269    compact_main_view_group(),
270    compact_replica_view_group(),
271
272    {View1QueryResult6, Group11}  = query_reduce_view(<<"view_1">>, false),
273    {View2QueryResult6, Group12} = query_reduce_view(<<"view_2">>, false),
274    etap:is(
275        View1QueryResult6,
276        ExpectedView1Result2,
277        "Reduce view has value " ++ couch_util:to_list(ExpectedView1Result2)),
278    etap:is(
279        View2QueryResult6,
280        ExpectedView2Result2,
281        "Reduce view 2 has value " ++ couch_util:to_list(ExpectedView2Result2)),
282
283    verify_main_group_btrees_3(Group11),
284    verify_replica_group_btrees_3(Group11),
285    compare_groups(Group11, Group12),
286
287    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
288    couch_set_view_test_util:stop_server(),
289    ok.
290
291
292query_reduce_view(ViewName, Stale) ->
293    query_reduce_view(ViewName, Stale, []).
294
295query_reduce_view(ViewName, Stale, Partitions) ->
296    etap:diag("Querying reduce view " ++ binary_to_list(ViewName) ++ " with ?group=true"),
297    GroupReq = #set_view_group_req{
298        stale = Stale,
299        wanted_partitions = Partitions,
300        debug = true
301    },
302    {ok, View, Group, []} = couch_set_view:get_reduce_view(
303        test_set_name(), ddoc_id(), ViewName, GroupReq),
304    FoldFun = fun(Key, Red, Acc) -> {ok, [{Key, Red} | Acc]} end,
305    ViewArgs = #view_query_args{
306        run_reduce = true,
307        view_name = ViewName
308    },
309    {ok, Rows} = couch_set_view:fold_reduce(Group, View, FoldFun, [], ViewArgs),
310    couch_set_view:release_group(Group),
311    case Rows of
312    [{_Key, {json, RedValue}}] ->
313        {ejson:decode(RedValue), Group};
314    [] ->
315        {empty, Group}
316    end.
317
318
319verify_group_info_during_replicas_transfer(MainGroupInfo, RepGroupInfo) ->
320    etap:diag("Verifying main and replica group infos obtained "
321        "right after activating the replica partitions"),
322    MainActive = couch_util:get_value(active_partitions, MainGroupInfo),
323    Diff = ordsets:subtract(MainActive, lists:seq(0, 31)),
324    etap:is(
325        ordsets:intersection(MainActive, lists:seq(0, 31)),
326        lists:seq(0, 31),
327        "Main group had partitions [ 0 .. 31 ] as active partitions"),
328    etap:is(
329        couch_util:get_value(passive_partitions, MainGroupInfo),
330        ordsets:subtract(lists:seq(32, 63), Diff),
331        "Main group had [ 32 .. 63 ] - Diff as passive partitions"),
332    etap:is(
333        couch_util:get_value(cleanup_partitions, MainGroupInfo),
334        [],
335        "Main group had [ ] as cleanup partitions"),
336    etap:is(
337        couch_util:get_value(replica_partitions, MainGroupInfo),
338        ordsets:subtract(lists:seq(32, 63), Diff),
339        "Main group had [ 32 .. 63 ] - Diff as replica partitions"),
340    etap:is(
341        couch_util:get_value(replicas_on_transfer, MainGroupInfo),
342        ordsets:subtract(lists:seq(32, 63), Diff),
343        "Main group had [ 32 .. 63 ] - Diff as replicas on transfer"),
344    etap:is(
345        couch_util:get_value(active_partitions, RepGroupInfo),
346        ordsets:subtract(lists:seq(32, 63), Diff),
347        "Replica group had [ 32 .. 63 ] - Diff as active partitions"),
348    etap:is(
349        couch_util:get_value(passive_partitions, RepGroupInfo),
350        [],
351        "Replica group had [ ] as passive partitions"),
352    etap:is(
353        couch_util:get_value(cleanup_partitions, RepGroupInfo),
354        [],
355        "Replica group had [ ] as cleanup partitions").
356
357
358verify_group_info_after_replicas_transfer(MainGroupInfo, RepGroupInfo) ->
359    etap:diag("Verifying main and replica group infos obtained "
360        "after the replica partitions were transferred"),
361    etap:is(
362        couch_util:get_value(active_partitions, MainGroupInfo),
363        lists:seq(0, 63),
364        "Main group had partitions [ 0 .. 63 ] as active partitions"),
365    etap:is(
366        couch_util:get_value(passive_partitions, MainGroupInfo),
367        [],
368        "Main group has [ ] as passive partitions"),
369    etap:is(
370        couch_util:get_value(cleanup_partitions, MainGroupInfo),
371        [],
372        "Main group has [ ] as cleanup partitions"),
373    etap:is(
374        couch_util:get_value(replica_partitions, MainGroupInfo),
375        [],
376        "Main group has [ ] as replica partitions"),
377    etap:is(
378        couch_util:get_value(replicas_on_transfer, MainGroupInfo),
379        [],
380        "Main group has [ ] as replicas on transfer"),
381    etap:is(
382        couch_util:get_value(active_partitions, RepGroupInfo),
383        [],
384        "Replica group has [ ] as active partitions"),
385    etap:is(
386        couch_util:get_value(passive_partitions, RepGroupInfo),
387        [],
388        "Replica group has [ ] as passive partitions"),
389    etap:is(
390        couch_util:get_value(cleanup_partitions, RepGroupInfo),
391        [],
392        "Replica group has [ ] as cleanup partitions").
393
394
395wait_for_replica_full_update() ->
396    etap:diag("Waiting for a full replica group update"),
397    {Stats} = couch_util:get_value(stats, get_replica_group_info()),
398    Updates = couch_util:get_value(full_updates, Stats),
399    MainGroupPid = couch_set_view:get_group_pid(
400        mapreduce_view, test_set_name(), ddoc_id(), prod),
401    {ok, ReplicaGroupPid} = gen_server:call(MainGroupPid, replica_pid, infinity),
402    {ok, UpPid} = gen_server:call(ReplicaGroupPid, {start_updater, []}, infinity),
403    case is_pid(UpPid) of
404    true ->
405        ok;
406    false ->
407        etap:bail("Updater was not triggered")
408    end,
409    Ref = erlang:monitor(process, UpPid),
410    receive
411    {'DOWN', Ref, process, UpPid, {updater_finished, _}} ->
412        ok;
413    {'DOWN', Ref, process, UpPid, noproc} ->
414        ok;
415    {'DOWN', Ref, process, UpPid, Reason} ->
416        etap:bail("Failure updating replica group: " ++ couch_util:to_list(Reason))
417    after ?MAX_WAIT_TIME ->
418        etap:bail("Timeout waiting for replica group update")
419    end,
420    {Stats2} = couch_util:get_value(stats, get_replica_group_info()),
421    Updates2 = couch_util:get_value(full_updates, Stats2),
422    case Updates2 == (Updates + 1) of
423    true ->
424        ok;
425    false ->
426        etap:bail("Updater was not triggered")
427    end.
428
429
430wait_for_replica_cleanup() ->
431    etap:diag("Waiting for replica index cleanup to finish"),
432    MainGroupInfo = get_group_info(),
433    {RepGroupInfo} = couch_util:get_value(replica_group_info, MainGroupInfo),
434    Pid = spawn(fun() ->
435        wait_replica_cleanup_loop(RepGroupInfo)
436    end),
437    Ref = erlang:monitor(process, Pid),
438    receive
439    {'DOWN', Ref, process, Pid, normal} ->
440        ok;
441    {'DOWN', Ref, process, Pid, noproc} ->
442        ok;
443    {'DOWN', Ref, process, Pid, Reason} ->
444        etap:bail("Failure waiting for replica index cleanup: " ++ couch_util:to_list(Reason))
445    after ?MAX_WAIT_TIME ->
446        etap:bail("Timeout waiting for replica index cleanup")
447    end.
448
449
450wait_replica_cleanup_loop(GroupInfo) ->
451    case couch_util:get_value(cleanup_partitions, GroupInfo) of
452    [] ->
453        {Stats} = couch_util:get_value(stats, GroupInfo),
454        Cleanups = couch_util:get_value(cleanups, Stats),
455        etap:is(
456            (is_integer(Cleanups) andalso (Cleanups > 0)),
457            true,
458            "Replica group stats has at least 1 full cleanup");
459    _ ->
460        ok = timer:sleep(500),
461        MainGroupInfo = get_group_info(),
462        {NewRepGroupInfo} = couch_util:get_value(replica_group_info, MainGroupInfo),
463        wait_replica_cleanup_loop(NewRepGroupInfo)
464    end.
465
466
467wait_for_main_full_update(GroupInfo, ExpectedReduceValue1, ExpectedReduceValue2) ->
468    etap:diag("Waiting for a full main group update"),
469    {Stats} = couch_util:get_value(stats, GroupInfo),
470    Updates = couch_util:get_value(full_updates, Stats),
471    Pid = spawn(fun() ->
472        NumQueries = wait_main_update_loop(
473            Updates, ExpectedReduceValue1, ExpectedReduceValue2, lists:seq(0, 63), 0),
474        % This assertion works as an alarm. Normally NumQueries varies
475        % per test run but it's always strictly greater than 0.
476        % On 2 different machines/hardware, it's normally greater than 100,
477        % which is most than enough for this test's purpose.
478        etap:is(NumQueries > 0, true,
479            "At least one query was done while the replica partitions data" ++
480            " was being transferred"),
481        etap:diag("Performed " ++ integer_to_list(NumQueries) ++
482            " queries while the replica partitions were being" ++
483            " transferred from the replica group to main group.")
484    end),
485    Ref = erlang:monitor(process, Pid),
486    receive
487    {'DOWN', Ref, process, Pid, normal} ->
488        ok;
489    {'DOWN', Ref, process, Pid, Reason} ->
490        etap:bail("Failure waiting for full main group update: " ++ couch_util:to_list(Reason))
491    after ?MAX_WAIT_TIME ->
492        etap:bail("Timeout waiting for main group update")
493    end.
494
495
496wait_main_update_loop(Updates, ExpectedReduceValue1, ExpectedReduceValue2, ExpectedPartitions, NumQueriesDone) ->
497    MainGroupInfo = get_group_info(),
498    {Stats} = couch_util:get_value(stats, MainGroupInfo),
499    case couch_util:get_value(full_updates, Stats) > Updates of
500    true ->
501        NumQueriesDone;
502    false ->
503        {RedValue1, _} = query_reduce_view(<<"view_1">>, false, ExpectedPartitions),
504        {RedValue2, _} = query_reduce_view(<<"view_2">>, false, ExpectedPartitions),
505        case RedValue1 =:= ExpectedReduceValue1 of
506        true ->
507            etap:diag("Reduce view 1 returned expected value " ++
508                couch_util:to_list(ExpectedReduceValue1));
509        false ->
510            etap:bail("Reduce view 1 did not return expected value " ++
511                couch_util:to_list(ExpectedReduceValue1) ++
512                ", got " ++ couch_util:to_list(RedValue1)),
513            exit(bad_reduce_value)
514        end,
515        case RedValue2 =:= ExpectedReduceValue2 of
516        true ->
517            etap:diag("Reduce view 2 returned expected value " ++
518                couch_util:to_list(ExpectedReduceValue2));
519        false ->
520            etap:bail("Reduce view 2 did not return expected value " ++
521                couch_util:to_list(ExpectedReduceValue2) ++
522                ", got " ++ couch_util:to_list(RedValue2)),
523            exit(bad_reduce_value)
524        end,
525        wait_main_update_loop(
526            Updates, ExpectedReduceValue1, ExpectedReduceValue2,
527            ExpectedPartitions, NumQueriesDone + 2)
528    end.
529
530
531get_group_info() ->
532    {ok, Info} = couch_set_view:get_group_info(
533        mapreduce_view, test_set_name(), ddoc_id(), prod),
534    Info.
535
536
537get_replica_group_info() ->
538    MainGroupInfo = get_group_info(),
539    {RepGroupInfo} = couch_util:get_value(replica_group_info, MainGroupInfo),
540    RepGroupInfo.
541
542
543doc_id(I) ->
544    iolist_to_binary(io_lib:format("doc_~8..0b", [I])).
545
546
547add_documents(StartId, Count) ->
548    etap:diag("Adding " ++ integer_to_list(Count) ++ " new documents"),
549    DocList0 = lists:map(
550        fun(I) ->
551            {I rem num_set_partitions(), {[
552                {<<"meta">>, {[{<<"id">>, doc_id(I)}]}},
553                {<<"json">>, {[
554                    {<<"value">>, I}
555                ]}}
556            ]}}
557        end,
558        lists:seq(StartId, StartId + Count - 1)),
559    DocList = [Doc || {_, Doc} <- lists:keysort(1, DocList0)],
560    ok = couch_set_view_test_util:populate_set_sequentially(
561        test_set_name(),
562        lists:seq(0, num_set_partitions() - 1),
563        DocList).
564
565
566create_set() ->
567    couch_set_view:cleanup_index_files(mapreduce_view, test_set_name()),
568    etap:diag("Populating the " ++ integer_to_list(num_set_partitions()) ++
569        " databases with " ++ integer_to_list(num_docs()) ++ " documents"),
570    DDoc = {[
571        {<<"meta">>, {[{<<"id">>, ddoc_id()}]}},
572        {<<"json">>, {[
573        {<<"language">>, <<"javascript">>},
574        {<<"views">>, {[
575            {<<"view_1">>, {[
576                {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value); }">>},
577                {<<"reduce">>, <<"_count">>}
578            ]}},
579            {<<"view_2">>, {[
580                {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value * 2); }">>},
581                {<<"reduce">>, <<"_sum">>}
582            ]}}
583        ]}}
584        ]}}
585    ]},
586    ok = couch_set_view_test_util:update_ddoc(test_set_name(), DDoc),
587    etap:diag("Configuring set view with partitions [0 .. 31] as active"),
588    Params = #set_view_params{
589        max_partitions = num_set_partitions(),
590        active_partitions = lists:seq(0, 31),
591        passive_partitions = [],
592        use_replica_index = true
593    },
594    ok = couch_set_view:define_group(
595        mapreduce_view, test_set_name(), ddoc_id(), Params).
596
597
598compact_main_view_group() ->
599    compact_view_group(main).
600
601compact_replica_view_group() ->
602    compact_view_group(replica).
603
604compact_view_group(Type) ->
605    {ok, CompactPid} = couch_set_view_compactor:start_compact(
606        mapreduce_view, test_set_name(), ddoc_id(), Type),
607    etap:diag("Waiting for " ++ atom_to_list(Type) ++ " view group compaction to finish"),
608    Ref = erlang:monitor(process, CompactPid),
609    receive
610    {'DOWN', Ref, process, CompactPid, normal} ->
611        ok;
612    {'DOWN', Ref, process, CompactPid, noproc} ->
613        ok;
614    {'DOWN', Ref, process, CompactPid, Reason} ->
615        etap:bail("Failure compacting " ++ atom_to_list(Type) ++ " group: " ++ couch_util:to_list(Reason))
616    after ?MAX_WAIT_TIME ->
617        etap:bail("Timeout waiting for " ++ atom_to_list(Type) ++ " group compaction to finish")
618    end.
619
620
621get_view(_ViewName, []) ->
622    undefined;
623get_view(ViewName, [SetView | Rest]) ->
624    RedFuns = (SetView#set_view.indexer)#mapreduce_view.reduce_funs,
625    case couch_util:get_value(ViewName, RedFuns) of
626    undefined ->
627        get_view(ViewName, Rest);
628    _ ->
629        SetView
630    end.
631
632
633verify_main_group_btrees_1(Group) ->
634    etap:diag("Verifying main view group"),
635    #set_view_group{
636        id_btree = IdBtree,
637        views = Views,
638        index_header = #set_view_index_header{
639            seqs = HeaderUpdateSeqs,
640            abitmask = Abitmask,
641            pbitmask = Pbitmask,
642            cbitmask = Cbitmask
643        }
644    } = Group,
645    etap:is(2, length(Views), "2 view btrees in the group"),
646    View1 = get_view(<<"view_1">>, Views),
647    View2 = get_view(<<"view_2">>, Views),
648    etap:isnt(View1, View2, "Views 1 and 2 have different btrees"),
649    #set_view{
650        indexer = #mapreduce_view{
651            btree = View1Btree
652        }
653    } = View1,
654    #set_view{
655        indexer = #mapreduce_view{
656            btree = View2Btree
657        }
658    } = View2,
659    ExpectedBitmask = couch_set_view_util:build_bitmask(lists:seq(0, 31)),
660    DbSeqs = couch_set_view_test_util:get_db_seqs(test_set_name(), lists:seq(0, 31)),
661
662    etap:is(
663        couch_set_view_test_util:full_reduce_id_btree(Group, IdBtree),
664        {ok, {num_docs() div 2, ExpectedBitmask}},
665        "Id Btree has the right reduce value"),
666    etap:is(
667        couch_set_view_test_util:full_reduce_view_btree(Group, View1Btree),
668        {ok, {num_docs() div 2, [num_docs() div 2], ExpectedBitmask}},
669        "View1 Btree has the right reduce value"),
670    ExpectedView2Reduction = [lists:sum(
671        [I * 2 || I <- lists:seq(0, num_docs() - 1), (I rem 64) < 32])],
672    etap:is(
673        couch_set_view_test_util:full_reduce_view_btree(Group, View2Btree),
674        {ok, {num_docs() div 2, ExpectedView2Reduction, ExpectedBitmask}},
675        "View2 Btree has the right reduce value"),
676
677    etap:is(HeaderUpdateSeqs, DbSeqs, "Header has right update seqs list"),
678    etap:is(Abitmask, ExpectedBitmask, "Header has right active bitmask"),
679    etap:is(Pbitmask, 0, "Header has right passive bitmask"),
680    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
681
682    etap:diag("Verifying the Id Btree"),
683    MaxPerPart = num_docs() div num_set_partitions(),
684    {ok, _, {_, _, _, IdBtreeFoldResult}} = couch_set_view_test_util:fold_id_btree(
685        Group,
686        IdBtree,
687        fun(Kv, _, {P0, I0, C0, It}) ->
688            case C0 >= MaxPerPart of
689            true ->
690                P = P0 + 1,
691                I = P,
692                C = 1;
693            false ->
694                P = P0,
695                I = I0,
696                C = C0 + 1
697            end,
698            true = (P < num_set_partitions()),
699            Value = [
700                 {View2#set_view.id_num, doc_id(I)},
701                 {View1#set_view.id_num, doc_id(I)}
702            ],
703            ExpectedKv = {<<P:16, (doc_id(I))/binary>>, {P, Value}},
704            case ExpectedKv =:= Kv of
705            true ->
706                ok;
707            false ->
708                etap:bail("Id Btree has an unexpected KV at iteration " ++ integer_to_list(It))
709            end,
710            {ok, {P, I + num_set_partitions(), C, It + 1}}
711        end,
712        {0, 0, 0, 0}, []),
713    etap:is(IdBtreeFoldResult, (num_docs() div 2),
714        "Id Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries"),
715
716    etap:diag("Verifying the View1 Btree"),
717    {ok, _, {_, View1BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
718        Group,
719        View1Btree,
720        fun(Kv, _, {NextId, I}) ->
721            PartId = NextId rem 64,
722            ExpectedKv = {
723                {doc_id(NextId), doc_id(NextId)},
724                {PartId, NextId}
725            },
726            case ExpectedKv =:= Kv of
727            true ->
728                ok;
729            false ->
730                etap:bail("View1 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
731            end,
732            case PartId =:= 31 of
733            true ->
734                {ok, {NextId + 33, I + 1}};
735            false ->
736                {ok, {NextId + 1, I + 1}}
737            end
738        end,
739        {0, 0}, []),
740    etap:is(View1BtreeFoldResult, (num_docs() div 2),
741        "View1 Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries"),
742
743    etap:diag("Verifying the View2 Btree"),
744    {ok, _, {_, View2BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
745        Group,
746        View2Btree,
747        fun(Kv, _, {NextId, I}) ->
748            PartId = NextId rem 64,
749            ExpectedKv = {
750                {doc_id(NextId), doc_id(NextId)},
751                {PartId, NextId * 2}
752            },
753            case ExpectedKv =:= Kv of
754            true ->
755                ok;
756            false ->
757                etap:bail("View2 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
758            end,
759            case PartId =:= 31 of
760            true ->
761                {ok, {NextId + 33, I + 1}};
762            false ->
763                {ok, {NextId + 1, I + 1}}
764            end
765        end,
766        {0, 0}, []),
767    etap:is(View2BtreeFoldResult, (num_docs() div 2),
768        "View2 Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries").
769
770
771verify_replica_group_btrees_1(MainGroup) ->
772    etap:diag("Verifying replica view group"),
773    etap:is(
774        MainGroup#set_view_group.replica_group,
775        nil,
776        "Main group points to a nil replica group"),
777    {ok, RepGroup, 0} = gen_server:call(
778        MainGroup#set_view_group.replica_pid,
779        #set_view_group_req{stale = ok, debug = true}),
780    #set_view_group{
781        id_btree = IdBtree,
782        views = Views,
783        index_header = #set_view_index_header{
784            seqs = HeaderUpdateSeqs,
785            abitmask = Abitmask,
786            pbitmask = Pbitmask,
787            cbitmask = Cbitmask
788        }
789    } = RepGroup,
790    etap:is(2, length(Views), "2 view btrees in the group"),
791    View1 = get_view(<<"view_1">>, Views),
792    View2 = get_view(<<"view_2">>, Views),
793    etap:isnt(View1, View2, "Views 1 and 2 have different btrees"),
794    #set_view{
795        indexer = #mapreduce_view{
796            btree = View1Btree
797        }
798    } = View1,
799    #set_view{
800        indexer = #mapreduce_view{
801            btree = View2Btree
802        }
803    } = View2,
804
805    etap:is(
806        couch_set_view_test_util:full_reduce_id_btree(MainGroup, IdBtree),
807        {ok, {0, 0}},
808        "Id Btree has the right reduce value"),
809    etap:is(
810        couch_set_view_test_util:full_reduce_view_btree(MainGroup, View1Btree),
811        {ok, {0, [0], 0}},
812        "View1 Btree has the right reduce value"),
813    etap:is(
814        couch_set_view_test_util:full_reduce_view_btree(MainGroup, View2Btree),
815        {ok, {0, [0], 0}},
816        "View2 Btree has the right reduce value"),
817
818    etap:is(HeaderUpdateSeqs, [], "Header has right update seqs list"),
819    etap:is(Abitmask, 0, "Header has right active bitmask"),
820    etap:is(Pbitmask, 0, "Header has right passive bitmask"),
821    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
822
823    etap:diag("Verifying the Id Btree"),
824    {ok, _, IdBtreeFoldResult} = couch_btree:fold(
825        IdBtree,
826        fun(_Kv, _, I) ->
827            {ok, I + 1}
828        end,
829        0, []),
830    etap:is(IdBtreeFoldResult, 0, "Id Btree is empty"),
831
832    etap:diag("Verifying the View1 Btree"),
833    {ok, _, View1BtreeFoldResult} = couch_btree:fold(
834        View1Btree,
835        fun(_Kv, _, I) ->
836            {ok, I + 1}
837        end,
838        0, []),
839    etap:is(View1BtreeFoldResult, 0, "View1 Btree is empty"),
840
841    etap:diag("Verifying the View2 Btree"),
842    {ok, _, View2BtreeFoldResult} = couch_btree:fold(
843        View2Btree,
844        fun(_Kv, _, I) ->
845            {ok, I + 1}
846        end,
847        0, []),
848    etap:is(View2BtreeFoldResult, 0, "View2 Btree is empty").
849
850
851verify_main_group_btrees_2(Group) ->
852    verify_main_group_btrees_1(Group).
853
854
855verify_replica_group_btrees_2(MainGroup) ->
856    etap:diag("Verifying replica view group"),
857    etap:is(
858        MainGroup#set_view_group.replica_group,
859        nil,
860        "Main group points to a nil replica group"),
861    {ok, RepGroup, 0} = gen_server:call(
862        MainGroup#set_view_group.replica_pid,
863        #set_view_group_req{stale = ok, debug = true}),
864    #set_view_group{
865        id_btree = IdBtree,
866        views = Views,
867        index_header = #set_view_index_header{
868            seqs = HeaderUpdateSeqs,
869            abitmask = Abitmask,
870            pbitmask = Pbitmask,
871            cbitmask = Cbitmask
872        }
873    } = RepGroup,
874    etap:is(2, length(Views), "2 view btrees in the group"),
875    View1 = get_view(<<"view_1">>, Views),
876    View2 = get_view(<<"view_2">>, Views),
877    etap:isnt(View1, View2, "Views 1 and 2 have different btrees"),
878    #set_view{
879        indexer = #mapreduce_view{
880            btree = View1Btree
881        }
882    } = View1,
883    #set_view{
884        indexer = #mapreduce_view{
885            btree = View2Btree
886        }
887    } = View2,
888    ExpectedBitmask = couch_set_view_util:build_bitmask(lists:seq(32, 63)),
889    DbSeqs = couch_set_view_test_util:get_db_seqs(test_set_name(), lists:seq(32, 63)),
890
891    etap:is(
892        couch_set_view_test_util:full_reduce_id_btree(MainGroup, IdBtree),
893        {ok, {num_docs() div 2, ExpectedBitmask}},
894        "Id Btree has the right reduce value"),
895    etap:is(
896        couch_set_view_test_util:full_reduce_view_btree(MainGroup, View1Btree),
897        {ok, {num_docs() div 2, [num_docs() div 2], ExpectedBitmask}},
898        "View1 Btree has the right reduce value"),
899    ExpectedView2Reduction = [lists:sum(
900        [I * 2 || I <- lists:seq(0, num_docs() - 1), (I rem 64) > 31])],
901    etap:is(
902        couch_set_view_test_util:full_reduce_view_btree(MainGroup, View2Btree),
903        {ok, {num_docs() div 2, ExpectedView2Reduction, ExpectedBitmask}},
904        "View2 Btree has the right reduce value"),
905
906    etap:is(HeaderUpdateSeqs, DbSeqs, "Header has right update seqs list"),
907    etap:is(Abitmask, 0, "Header has right active bitmask"),
908    etap:is(Pbitmask, ExpectedBitmask, "Header has right passive bitmask"),
909    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
910
911    etap:diag("Verifying the Id Btree"),
912    MaxPerPart = num_docs() div num_set_partitions(),
913    {ok, _, {_, _, _, IdBtreeFoldResult}} = couch_set_view_test_util:fold_id_btree(
914        MainGroup,
915        IdBtree,
916        fun(Kv, _, {P0, I0, C0, It}) ->
917            case C0 >= MaxPerPart of
918            true ->
919                P = P0 + 1,
920                I = P,
921                C = 1;
922            false ->
923                P = P0,
924                I = I0,
925                C = C0 + 1
926            end,
927            true = (P < num_set_partitions()),
928            Value = [
929                 {View2#set_view.id_num, doc_id(I)},
930                 {View1#set_view.id_num, doc_id(I)}
931            ],
932            ExpectedKv = {<<P:16, (doc_id(I))/binary>>, {P, Value}},
933            case ExpectedKv =:= Kv of
934            true ->
935                ok;
936            false ->
937                etap:bail("Id Btree has an unexpected KV at iteration " ++ integer_to_list(It))
938            end,
939            {ok, {P, I + num_set_partitions(), C, It + 1}}
940        end,
941        {32, 32, 0, 0}, []),
942    etap:is(IdBtreeFoldResult, (num_docs() div 2),
943        "Id Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries"),
944
945    etap:diag("Verifying the View1 Btree"),
946    {ok, _, {_, View1BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
947        MainGroup,
948        View1Btree,
949        fun(Kv, _, {NextId, I}) ->
950            PartId = NextId rem 64,
951            ExpectedKv = {
952                {doc_id(NextId), doc_id(NextId)},
953                {PartId, NextId}
954            },
955            case ExpectedKv =:= Kv of
956            true ->
957                ok;
958            false ->
959                etap:bail("View1 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
960            end,
961            case PartId =:= 63 of
962            true ->
963                {ok, {NextId + 33, I + 1}};
964            false ->
965                {ok, {NextId + 1, I + 1}}
966            end
967        end,
968        {32, 0}, []),
969    etap:is(View1BtreeFoldResult, (num_docs() div 2),
970        "View1 Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries"),
971
972    etap:diag("Verifying the View2 Btree"),
973    {ok, _, {_, View2BtreeFoldResult}} = couch_set_view_test_util:fold_view_btree(
974        MainGroup,
975        View2Btree,
976        fun(Kv, _, {NextId, I}) ->
977            PartId = NextId rem 64,
978            ExpectedKv = {
979                {doc_id(NextId), doc_id(NextId)},
980                {PartId, NextId * 2}
981            },
982            case ExpectedKv =:= Kv of
983            true ->
984                ok;
985            false ->
986                etap:bail("View2 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
987            end,
988            case PartId =:= 63 of
989            true ->
990                {ok, {NextId + 33, I + 1}};
991            false ->
992                {ok, {NextId + 1, I + 1}}
993            end
994        end,
995        {32, 0}, []),
996    etap:is(View2BtreeFoldResult, (num_docs() div 2),
997        "View2 Btree has " ++ integer_to_list(num_docs() div 2) ++ " entries").
998
999
1000verify_main_group_btrees_3(Group) ->
1001    etap:diag("Verifying main view group"),
1002    #set_view_group{
1003        id_btree = IdBtree,
1004        views = Views,
1005        index_header = #set_view_index_header{
1006            seqs = HeaderUpdateSeqs,
1007            abitmask = Abitmask,
1008            pbitmask = Pbitmask,
1009            cbitmask = Cbitmask
1010        }
1011    } = Group,
1012    etap:is(2, length(Views), "2 view btrees in the group"),
1013    View1 = get_view(<<"view_1">>, Views),
1014    View2 = get_view(<<"view_2">>, Views),
1015    etap:isnt(View1, View2, "Views 1 and 2 have different btrees"),
1016    #set_view{
1017        indexer = #mapreduce_view{
1018            btree = View1Btree
1019        }
1020    } = View1,
1021    #set_view{
1022        indexer = #mapreduce_view{
1023            btree = View2Btree
1024        }
1025    } = View2,
1026    ExpectedBitmask = couch_set_view_util:build_bitmask(lists:seq(0, 63)),
1027    DbSeqs = couch_set_view_test_util:get_db_seqs(test_set_name(), lists:seq(0, 63)),
1028
1029    etap:is(
1030        couch_set_view_test_util:full_reduce_id_btree(Group, IdBtree),
1031        {ok, {num_docs(), ExpectedBitmask}},
1032        "Id Btree has the right reduce value"),
1033    etap:is(
1034        couch_set_view_test_util:full_reduce_view_btree(Group, View1Btree),
1035        {ok, {num_docs(), [num_docs()], ExpectedBitmask}},
1036        "View1 Btree has the right reduce value"),
1037    ExpectedView2Reduction = [lists:sum([I * 2 || I <- lists:seq(0, num_docs() - 1)])],
1038    etap:is(
1039        couch_set_view_test_util:full_reduce_view_btree(Group, View2Btree),
1040        {ok, {num_docs(), ExpectedView2Reduction, ExpectedBitmask}},
1041        "View2 Btree has the right reduce value"),
1042
1043    etap:is(HeaderUpdateSeqs, DbSeqs, "Header has right update seqs list"),
1044    etap:is(Abitmask, ExpectedBitmask, "Header has right active bitmask"),
1045    etap:is(Pbitmask, 0, "Header has right passive bitmask"),
1046    etap:is(Cbitmask, 0, "Header has right cleanup bitmask"),
1047
1048    etap:diag("Verifying the Id Btree"),
1049    MaxPerPart = num_docs() div num_set_partitions(),
1050    {ok, _, {_, _, _, IdBtreeFoldResult}} = couch_set_view_test_util:fold_id_btree(
1051        Group,
1052        IdBtree,
1053        fun(Kv, _, {P0, I0, C0, It}) ->
1054            case C0 >= MaxPerPart of
1055            true ->
1056                P = P0 + 1,
1057                I = P,
1058                C = 1;
1059            false ->
1060                P = P0,
1061                I = I0,
1062                C = C0 + 1
1063            end,
1064            true = (P < num_set_partitions()),
1065            Value = [
1066                 {View2#set_view.id_num, doc_id(I)},
1067                 {View1#set_view.id_num, doc_id(I)}
1068            ],
1069            ExpectedKv = {<<P:16, (doc_id(I))/binary>>, {P, Value}},
1070            case ExpectedKv =:= Kv of
1071            true ->
1072                ok;
1073            false ->
1074                etap:bail("Id Btree has an unexpected KV at iteration " ++ integer_to_list(It))
1075            end,
1076            {ok, {P, I + num_set_partitions(), C, It + 1}}
1077        end,
1078        {0, 0, 0, 0}, []),
1079    etap:is(IdBtreeFoldResult, num_docs(),
1080        "Id Btree has " ++ integer_to_list(num_docs()) ++ " entries"),
1081
1082    etap:diag("Verifying the View1 Btree"),
1083    {ok, _, View1BtreeFoldResult} = couch_set_view_test_util:fold_view_btree(
1084        Group,
1085        View1Btree,
1086        fun(Kv, _, I) ->
1087            PartId = I rem 64,
1088            ExpectedKv = {{doc_id(I), doc_id(I)}, {PartId, I}},
1089            case ExpectedKv =:= Kv of
1090            true ->
1091                ok;
1092            false ->
1093                etap:bail("View1 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
1094            end,
1095            {ok, I + 1}
1096        end,
1097        0, []),
1098    etap:is(View1BtreeFoldResult, num_docs(),
1099        "View1 Btree has " ++ integer_to_list(num_docs()) ++ " entries"),
1100
1101    etap:diag("Verifying the View2 Btree"),
1102    {ok, _, View2BtreeFoldResult} = couch_set_view_test_util:fold_view_btree(
1103        Group,
1104        View2Btree,
1105        fun(Kv, _, I) ->
1106            PartId = I rem 64,
1107            ExpectedKv = {{doc_id(I), doc_id(I)}, {PartId, I * 2}},
1108            case ExpectedKv =:= Kv of
1109            true ->
1110                ok;
1111            false ->
1112                etap:bail("View2 Btree has an unexpected KV at iteration " ++ integer_to_list(I))
1113            end,
1114            {ok, I + 1}
1115        end,
1116        0, []),
1117    etap:is(View2BtreeFoldResult, num_docs(),
1118        "View2 Btree has " ++ integer_to_list(num_docs()) ++ " entries").
1119
1120
1121verify_replica_group_btrees_3(MainGroup) ->
1122    verify_replica_group_btrees_1(MainGroup).
1123
1124
1125compare_groups(Group1, Group2) ->
1126    etap:is(
1127        Group2#set_view_group.views,
1128        Group1#set_view_group.views,
1129        "View states are equal"),
1130    etap:is(
1131        Group2#set_view_group.index_header,
1132        Group1#set_view_group.index_header,
1133        "Index headers are equal").
1134