1#!/usr/bin/env escript
2%% -*- erlang -*-
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-record(user_ctx, {
18    name = null,
19    roles = [],
20    handler
21}).
22
23-record(db, {
24    main_pid = nil,
25    update_pid = nil,
26    compactor_pid = nil,
27    instance_start_time, % number of microsecs since jan 1 1970 as a binary string
28    fd,
29    fd_ref_counter,
30    header = nil,
31    committed_update_seq,
32    fulldocinfo_by_id_btree,
33    docinfo_by_seq_btree,
34    local_docs_btree,
35    update_seq,
36    name,
37    filepath,
38    security = [],
39    security_ptr = nil,
40    user_ctx = #user_ctx{},
41    waiting_delayed_commit = nil,
42    fsync_options = [],
43    options = []
44}).
45
46main_db_name() -> <<"couch_test_view_group_shutdown">>.
47
48
49main(_) ->
50    test_util:init_code_path(),
51
52    etap:plan(13),
53    case (catch test()) of
54        ok ->
55            etap:end_tests();
56        Other ->
57            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
58            etap:bail(Other)
59    end,
60    ok.
61
62
63test() ->
64    couch_server_sup:start_link(test_util:config_files()),
65    ok = couch_config:set("couchdb", "delayed_commits", "false", false),
66    crypto:start(),
67
68    % Test that while a view group is being compacted its database can not
69    % be closed by the database LRU system.
70    test_view_group_compaction(),
71
72    couch_server_sup:stop(),
73    ok.
74
75
76test_view_group_compaction() ->
77    {ok, DbWriter3} = create_db(<<"couch_test_view_group_shutdown_w3">>),
78    ok = couch_db:close(DbWriter3),
79
80    {ok, MainDb} = create_main_db(),
81    ok = couch_db:close(MainDb),
82
83    {ok, DbWriter1} = create_db(<<"couch_test_view_group_shutdown_w1">>),
84    ok = couch_db:close(DbWriter1),
85
86    {ok, DbWriter2} = create_db(<<"couch_test_view_group_shutdown_w2">>),
87    ok = couch_db:close(DbWriter2),
88
89    Writer1 = spawn_writer(DbWriter1#db.name),
90    Writer2 = spawn_writer(DbWriter2#db.name),
91    etap:is(is_process_alive(Writer1), true, "Spawned writer 1"),
92    etap:is(is_process_alive(Writer2), true, "Spawned writer 2"),
93
94    etap:is(get_writer_status(Writer1), ok, "Writer 1 opened his database"),
95    etap:is(get_writer_status(Writer2), ok, "Writer 2 opened his database"),
96
97    {ok, CompactPid} = couch_view_compactor:start_compact(
98        MainDb#db.name, <<"foo">>),
99    MonRef = erlang:monitor(process, CompactPid),
100
101    % Add some more docs to database and trigger view update
102    {ok, MainDb2} = couch_db:open_int(MainDb#db.name, []),
103    ok = populate_main_db(MainDb2, 3, 3),
104    update_view(MainDb2#db.name, <<"_design/foo">>, <<"foo">>),
105    ok = couch_db:close(MainDb2),
106
107    Writer3 = spawn_writer(DbWriter3#db.name),
108
109    etap:is(is_process_alive(Writer1), true, "Writer 1 still alive"),
110    etap:is(is_process_alive(Writer2), true, "Writer 2 still alive"),
111    etap:is(is_process_alive(Writer3), true, "Writer 3 still alive"),
112
113    receive
114    {'DOWN', MonRef, process, CompactPid, normal} ->
115         etap:diag("View group compaction successful"),
116         ok;
117    {'DOWN', MonRef, process, CompactPid, _Reason} ->
118         etap:bail("Failure compacting view group")
119    end,
120
121    etap:is(is_process_alive(Writer1), true, "Writer 1 still alive"),
122    etap:is(is_process_alive(Writer2), true, "Writer 2 still alive"),
123    etap:is(is_process_alive(Writer3), true, "Writer 3 still alive"),
124
125    etap:is(stop_writer(Writer1), ok, "Stopped writer 1"),
126    etap:is(stop_writer(Writer2), ok, "Stopped writer 2"),
127    etap:is(stop_writer(Writer3), ok, "Stopped writer 3"),
128
129    delete_db(MainDb),
130    delete_db(DbWriter1),
131    delete_db(DbWriter2),
132    delete_db(DbWriter3).
133
134
135create_main_db() ->
136    {ok, Db} = create_db(main_db_name()),
137    DDoc = couch_doc:from_json_obj({[
138        {<<"meta">>, {[
139            {<<"id">>, <<"_design/foo">>}
140        ]}},
141        {<<"json">>, {[
142            {<<"language">>, <<"javascript">>},
143            {<<"views">>, {[
144                {<<"foo">>, {[
145                    {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
146                ]}},
147                {<<"foo2">>, {[
148                    {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
149                ]}},
150                {<<"foo3">>, {[
151                    {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
152                ]}},
153                {<<"foo4">>, {[
154                    {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
155                ]}},
156                {<<"foo5">>, {[
157                    {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>}
158                ]}}
159            ]}}
160        ]}}
161    ]}),
162    ok = couch_db:update_doc(Db, DDoc, []),
163    ok = populate_main_db(Db, 1000, 20000),
164    update_view(Db#db.name, <<"_design/foo">>, <<"foo">>),
165    {ok, Db}.
166
167
168populate_main_db(Db, BatchSize, N) when N > 0 ->
169    Docs = lists:map(
170        fun(_) ->
171            couch_doc:from_json_obj({[
172                {<<"meta">>, {[
173                    {<<"id">>, couch_uuids:new()}
174                ]}},
175                {<<"json">>, {[
176                    {<<"value">>, base64:encode(crypto:rand_bytes(1000))}
177                ]}}
178            ]})
179        end,
180        lists:seq(1, BatchSize)),
181    ok = couch_db:update_docs(Db, Docs, [sort_docs]),
182    populate_main_db(Db, BatchSize, N - length(Docs));
183populate_main_db(_Db, _, _) ->
184    ok.
185
186
187update_view(DbName, DDocName, ViewName) ->
188    % Use a dedicated process -  we can't explicitly drop the #group ref counter
189    Pid = spawn(fun() ->
190        {ok, Db} = couch_db:open_int(DbName, []),
191        couch_view:get_map_view(Db, DDocName, ViewName, false),
192        ok = couch_db:close(Db)
193    end),
194    MonRef = erlang:monitor(process, Pid),
195    receive
196    {'DOWN', MonRef, process, Pid, normal} ->
197         etap:diag("View group updated"),
198         ok;
199    {'DOWN', MonRef, process, Pid, _Reason} ->
200         etap:bail("Failure updating view group")
201    end.
202
203create_db(DbName) ->
204    {ok, Db} = couch_db:create(
205        DbName,
206        [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]),
207    {ok, Db}.
208
209
210delete_db(#db{name = DbName, main_pid = Pid}) ->
211    ok = couch_server:delete(
212        DbName, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]),
213    MonRef = erlang:monitor(process, Pid),
214    receive
215    {'DOWN', MonRef, process, Pid, _Reason} ->
216        ok
217    after 30000 ->
218        etap:bail("Timeout deleting database")
219    end.
220
221
222spawn_writer(DbName) ->
223    Parent = self(),
224    spawn(fun() ->
225        process_flag(priority, high),
226        writer_loop(DbName, Parent)
227    end).
228
229
230get_writer_status(Writer) ->
231    Ref = make_ref(),
232    Writer ! {get_status, Ref},
233    receive
234    {db_open, Ref} ->
235        ok;
236    {db_open_error, Error, Ref} ->
237        Error
238    after 5000 ->
239        timeout
240    end.
241
242
243writer_try_again(Writer) ->
244    Ref = make_ref(),
245    Writer ! {try_again, Ref},
246    receive
247    {ok, Ref} ->
248        ok
249    after 5000 ->
250        timeout
251    end.
252
253
254stop_writer(Writer) ->
255    Ref = make_ref(),
256    Writer ! {stop, Ref},
257    receive
258    {ok, Ref} ->
259        ok
260    after 5000 ->
261        etap:bail("Timeout stopping writer process")
262    end.
263
264
265% Just keep the database open, no need to actually do something on it.
266writer_loop(DbName, Parent) ->
267    case couch_db:open_int(DbName, []) of
268    {ok, Db} ->
269        writer_loop_1(Db, Parent);
270    Error ->
271        writer_loop_2(DbName, Parent, Error)
272    end.
273
274writer_loop_1(Db, Parent) ->
275    receive
276    {get_status, Ref} ->
277        Parent ! {db_open, Ref},
278        writer_loop_1(Db, Parent);
279    {stop, Ref} ->
280        ok = couch_db:close(Db),
281        Parent ! {ok, Ref}
282    end.
283
284writer_loop_2(DbName, Parent, Error) ->
285    receive
286    {get_status, Ref} ->
287        Parent ! {db_open_error, Error, Ref},
288        writer_loop_2(DbName, Parent, Error);
289    {try_again, Ref} ->
290        Parent ! {ok, Ref},
291        writer_loop(DbName, Parent)
292    end.
293