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