Skip to content

Commit 2d12ab0

Browse files
authored
Avoid read docs twice when filtered _changes is triggered (#4862)
* Add more tests for couch_changes and fabric changes Add more tests to ensure the response is the same before and after the code change. Code coverage improvements - chttpd_changes_test.erl: 99% -> 100% - couch_changes.erl: 80% -> 82% - fabric_rpc.erl: 56% -> 61% * Verify the number of calls to `open_doc/3` when requesting `_changes` * Avoid read docs twice when filtered _changes is triggered - When filered `_changes` is triggered, `open_revs()` will read docs. If request parameter has `include_docs=true`, then `doc_member()` will also read docs. To avoid the above behavior, changed `filter/3` to `filter/5`. - When using filtered `_changes` with `style=all_docs`, `conflicts=true` and `include_docs=true`, the response should contain the `_conflicts` field. Add `open_all_revs_include_doc/2` to include `_conflicts` field. - If conflicted docs contain a deleted doc, the deleted `rev` value should not appear in the `doc` field. Add `Doc#doc.deleted =:= false` filter to avoid this behaviour. * Update tests to verify the number of calls to `open_doc/3`
1 parent 5c6aeab commit 2d12ab0

5 files changed

Lines changed: 1542 additions & 175 deletions

File tree

src/chttpd/test/eunit/chttpd_changes_test.erl

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,18 @@ changes_include_docs_test_() ->
153153
]
154154
}.
155155

156+
changes_open_doc_times_test_() ->
157+
{
158+
foreach,
159+
fun setup_basic/0,
160+
fun teardown_basic/1,
161+
[
162+
?TDEF_FE(t_selector_open_doc_times),
163+
?TDEF_FE(t_js_filter_open_doc_times),
164+
?TDEF_FE(t_view_open_doc_times)
165+
]
166+
}.
167+
156168
t_basic({_, DbUrl}) ->
157169
Res = {Seq, Pending, Rows} = changes(DbUrl),
158170
?assertEqual(8, Seq),
@@ -853,8 +865,39 @@ t_view_all_docs_conflicts_include_docs({_, DbUrl}) ->
853865
?assertEqual(Res1, Res2),
854866
delete_ddocs(DDocUrl, Rev).
855867

856-
% Utility functions
868+
t_selector_open_doc_times({_, DbUrl}) ->
869+
Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}},
870+
F = "?filter=_selector",
871+
I = "&include_docs=true",
872+
S = "&style=all_docs",
873+
Filter = called_times(fun() -> changes_post(DbUrl, Body, F) end, DbUrl),
874+
FilterDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I) end, DbUrl),
875+
FilterAllDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I ++ S) end, DbUrl),
876+
?assertEqual({3, 3, 5}, {Filter, FilterDocs, FilterAllDocs}).
877+
878+
t_js_filter_open_doc_times({_, DbUrl}) ->
879+
{DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, custom),
880+
F = "?filter=filters/f",
881+
I = "&include_docs=true",
882+
S = "&style=all_docs",
883+
Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl),
884+
FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl),
885+
FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ I ++ S) end, DbUrl),
886+
?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}),
887+
delete_ddocs(DDocUrl, Rev).
857888

889+
t_view_open_doc_times({_, DbUrl}) ->
890+
{DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view),
891+
F = "?filter=_view&view=views/v",
892+
I = "&include_docs=true",
893+
S = "&style=all_docs",
894+
Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl),
895+
FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl),
896+
FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ S ++ I) end, DbUrl),
897+
?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}),
898+
delete_ddocs(DDocUrl, Rev).
899+
900+
% Utility functions
858901
setup_ctx(DbCreateParams) ->
859902
Ctx = test_util:start_couch([chttpd]),
860903
Hashed = couch_passwords:hash_admin_password(?PASS),
@@ -883,6 +926,7 @@ setup_basic() ->
883926
CfgKey = "changes_doc_ids_optimization_threshold",
884927
ok = config:set("couchdb", CfgKey, "2", _Persist = false),
885928
meck:new(couch_changes, [passthrough]),
929+
meck:new(couch_db, [passthrough]),
886930
{Ctx, DbUrl}.
887931

888932
teardown_basic({Ctx, DbUrl}) ->
@@ -891,20 +935,11 @@ teardown_basic({Ctx, DbUrl}) ->
891935
teardown_ctx({Ctx, DbUrl}).
892936

893937
create_db(Top, Db, Params) ->
894-
case req(put, Top ++ Db ++ Params) of
895-
{201, #{}} ->
896-
ok;
897-
Error ->
898-
error({failed_to_create_test_db, Db, Error})
899-
end.
938+
{201, #{}} = req(put, Top ++ Db ++ Params),
939+
ok.
900940

901941
delete_db(DbUrl) ->
902-
case req(delete, DbUrl) of
903-
{200, #{}} ->
904-
ok;
905-
Error ->
906-
error({failed_to_delete_test_db, DbUrl, Error})
907-
end.
942+
{200, #{}} = req(delete, DbUrl).
908943

909944
doc_fun({Id, Revs, Deleted}) ->
910945
Doc = #{
@@ -1033,3 +1068,23 @@ seq(<<_/binary>> = Seq) ->
10331068
binary_to_integer(NumStr);
10341069
seq(null) ->
10351070
null.
1071+
1072+
called_times(ReqFun, DbUrl) ->
1073+
meck:reset(couch_db),
1074+
ReqFun(),
1075+
open_doc_calls(DbUrl).
1076+
1077+
open_doc_calls(DbUrl) ->
1078+
#{path := "/" ++ DbName0} = uri_string:parse(DbUrl),
1079+
DbName = ?l2b(DbName0),
1080+
FoldFun =
1081+
fun([Db, IdOrDocInfo, _Opts], Acc) ->
1082+
case {mem3:dbname(couch_db:name(Db)), IdOrDocInfo} of
1083+
{DbName, #doc_info{}} -> Acc + 1;
1084+
_ -> Acc
1085+
end
1086+
end,
1087+
lists:foldl(FoldFun, 0, meck_history(couch_db, open_doc, 3)).
1088+
1089+
meck_history(Mod, Fun, Arity) ->
1090+
[A || {_Pid, {_M, F, A}, _R} <- meck:history(Mod), F =:= Fun, length(A) =:= Arity].

src/couch/src/couch_changes.erl

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
wait_updated/3,
2020
get_rest_updated/1,
2121
configure_filter/4,
22-
filter/3,
22+
filter/5,
2323
handle_db_event/3,
2424
handle_view_event/3,
2525
send_changes_doc_ids/6,
@@ -225,44 +225,71 @@ configure_filter(FilterName, Style, Req, Db) ->
225225
throw({bad_request, Msg})
226226
end.
227227

228-
filter(Db, #full_doc_info{} = FDI, Filter) ->
229-
filter(Db, couch_doc:to_doc_info(FDI), Filter);
230-
filter(_Db, DocInfo, {default, Style}) ->
231-
apply_style(DocInfo, Style);
232-
filter(_Db, DocInfo, {doc_ids, Style, DocIds}) ->
233-
case lists:member(DocInfo#doc_info.id, DocIds) of
234-
true ->
235-
apply_style(DocInfo, Style);
236-
false ->
237-
[]
238-
end;
239-
filter(Db, DocInfo, {selector, Style, {Selector, _Fields}}) ->
240-
Docs = open_revs(Db, DocInfo, Style),
241-
Passes = [
242-
mango_selector:match(Selector, couch_doc:to_json_obj(Doc, []))
243-
|| Doc <- Docs
244-
],
245-
filter_revs(Passes, Docs);
246-
filter(_Db, DocInfo, {design_docs, Style}) ->
247-
case DocInfo#doc_info.id of
248-
<<"_design", _/binary>> ->
249-
apply_style(DocInfo, Style);
250-
_ ->
251-
[]
228+
filter(Db, #full_doc_info{} = FDI, Filter, IncludeDocs, Conflicts) ->
229+
filter(Db, couch_doc:to_doc_info(FDI), Filter, IncludeDocs, Conflicts);
230+
filter(_Db, DocInfo, {default, Style}, _IncludeDocs, _Conflicts) ->
231+
{[], apply_style(DocInfo, Style)};
232+
filter(_Db, DocInfo, {doc_ids, Style, DocIds}, _IncludeDocs, _Conflicts) ->
233+
Revs =
234+
case lists:member(DocInfo#doc_info.id, DocIds) of
235+
true ->
236+
apply_style(DocInfo, Style);
237+
false ->
238+
[]
239+
end,
240+
{[], Revs};
241+
filter(_Db, DocInfo, {design_docs, Style}, _IncludeDocs, _Conflicts) ->
242+
Revs =
243+
case DocInfo#doc_info.id of
244+
<<"_design", _/binary>> ->
245+
apply_style(DocInfo, Style);
246+
_ ->
247+
[]
248+
end,
249+
{[], Revs};
250+
filter(Db, DocInfo, {selector, all_docs, {Selector, _Fields}}, true, true) ->
251+
{DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo),
252+
Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, [])) || Doc <- Docs],
253+
{DocWin, filter_revs(Passes, Docs)};
254+
filter(Db, DocInfo, {selector, Style, {Selector, _Fields}}, IncludeDocs, Conflicts) ->
255+
Docs = open_revs(Db, DocInfo, Style, Conflicts),
256+
Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, [])) || Doc <- Docs],
257+
case IncludeDocs of
258+
true -> {Docs, filter_revs(Passes, Docs)};
259+
false -> {[], filter_revs(Passes, Docs)}
252260
end;
253-
filter(Db, DocInfo, {view, Style, DDoc, VName}) ->
254-
Docs = open_revs(Db, DocInfo, Style),
261+
filter(Db, DocInfo, {view, all_docs, DDoc, VName}, true, true) ->
262+
{DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo),
255263
{ok, Passes} = couch_query_servers:filter_view(Db, DDoc, VName, Docs),
256-
filter_revs(Passes, Docs);
257-
filter(Db, DocInfo, {custom, Style, Req0, DDoc, FName}) ->
264+
{DocWin, filter_revs(Passes, Docs)};
265+
filter(Db, DocInfo, {view, Style, DDoc, VName}, IncludeDocs, Conflicts) ->
266+
Docs = open_revs(Db, DocInfo, Style, Conflicts),
267+
{ok, Passes} = couch_query_servers:filter_view(Db, DDoc, VName, Docs),
268+
case IncludeDocs of
269+
true -> {Docs, filter_revs(Passes, Docs)};
270+
false -> {[], filter_revs(Passes, Docs)}
271+
end;
272+
filter(Db, DocInfo, {custom, all_docs, Req0, DDoc, FName}, true, true) ->
273+
Req =
274+
case Req0 of
275+
{json_req, _} -> Req0;
276+
#httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)}
277+
end,
278+
{DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo),
279+
{ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs),
280+
{DocWin, filter_revs(Passes, Docs)};
281+
filter(Db, DocInfo, {custom, Style, Req0, DDoc, FName}, IncludeDocs, Conflicts) ->
258282
Req =
259283
case Req0 of
260284
{json_req, _} -> Req0;
261285
#httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)}
262286
end,
263-
Docs = open_revs(Db, DocInfo, Style),
287+
Docs = open_revs(Db, DocInfo, Style, Conflicts),
264288
{ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs),
265-
filter_revs(Passes, Docs).
289+
case IncludeDocs of
290+
true -> {Docs, filter_revs(Passes, Docs)};
291+
false -> {[], filter_revs(Passes, Docs)}
292+
end.
266293

267294
get_view_qs({json_req, {Props}}) ->
268295
{Query} = couch_util:get_value(<<"query">>, Props, {[]}),
@@ -357,17 +384,32 @@ apply_style(#doc_info{revs = Revs}, main_only) ->
357384
apply_style(#doc_info{revs = Revs}, all_docs) ->
358385
[{[{<<"rev">>, couch_doc:rev_to_str(R)}]} || #rev_info{rev = R} <- Revs].
359386

360-
open_revs(Db, DocInfo, Style) ->
387+
open_revs(Db, DocInfo, Style, Conflicts) ->
361388
DocInfos =
362389
case Style of
363390
main_only -> [DocInfo];
364391
all_docs -> [DocInfo#doc_info{revs = [R]} || R <- DocInfo#doc_info.revs]
365392
end,
366-
OpenOpts = [deleted, conflicts],
393+
OpenOpts =
394+
case Conflicts of
395+
true -> [deleted, conflicts];
396+
false -> [deleted]
397+
end,
367398
% Relying on list comprehensions to silence errors
368399
OpenResults = [couch_db:open_doc(Db, DI, OpenOpts) || DI <- DocInfos],
369400
[Doc || {ok, Doc} <- OpenResults].
370401

402+
open_all_revs_include_doc(Db, DocInfo) ->
403+
DocInfos = [DocInfo#doc_info{revs = [R]} || R <- DocInfo#doc_info.revs],
404+
OpenOpts = [deleted, conflicts],
405+
OpenResults = [couch_db:open_doc(Db, DI, OpenOpts) || DI <- DocInfos],
406+
Docs = [Doc || {ok, Doc} <- OpenResults],
407+
[Doc1 | RestDocs] = Docs,
408+
RestRevs = [Doc#doc.revs || Doc <- RestDocs, Doc#doc.deleted =:= false],
409+
Conflicts = [{conflicts, [{Pos, RevId} || {Pos, [RevId]} <- RestRevs]}],
410+
Doc2 = Doc1#doc{meta = Conflicts},
411+
{[Doc2], Docs}.
412+
371413
filter_revs(Passes, Docs) ->
372414
lists:flatmap(
373415
fun
@@ -611,12 +653,14 @@ changes_enumerator(Value, Acc) ->
611653
prepend = Prepend,
612654
user_acc = UserAcc,
613655
limit = Limit,
656+
include_docs = IncludeDocs,
657+
conflicts = Conflicts,
614658
resp_type = ResponseType,
615659
db = Db,
616660
timeout = Timeout,
617661
timeout_fun = TimeoutFun
618662
} = maybe_upgrade_changes_acc(Acc),
619-
Results0 = filter(Db, Value, Filter),
663+
{_, Results0} = filter(Db, Value, Filter, IncludeDocs, Conflicts),
620664
Results = [Result || Result <- Results0, Result /= null],
621665
Seq =
622666
case Value of

0 commit comments

Comments
 (0)