diff options
author | Paul J. Davis <paul.joseph.davis@gmail.com> | 2020-03-25 15:16:22 -0500 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2020-04-10 16:30:49 -0500 |
commit | 757515231ff3d2e0228a078a1771837eff0b66d0 (patch) | |
tree | 2e1f57611bf31e0e69469f3d6fc41cd68a82e792 | |
parent | 30fdef77571c67c945d70fb54c07157c4643f828 (diff) | |
download | couchdb-757515231ff3d2e0228a078a1771837eff0b66d0.tar.gz |
Implement couch_views_cleanup_test.erl
Add tests for view cleanup.
-rw-r--r-- | src/couch_views/test/couch_views_cleanup_test.erl | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/src/couch_views/test/couch_views_cleanup_test.erl b/src/couch_views/test/couch_views_cleanup_test.erl new file mode 100644 index 000000000..b5e081a98 --- /dev/null +++ b/src/couch_views/test/couch_views_cleanup_test.erl @@ -0,0 +1,411 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_views_cleanup_test). + + +-include_lib("couch/include/couch_db.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("couch_views/include/couch_views.hrl"). +-include_lib("couch_mrview/include/couch_mrview.hrl"). +-include_lib("fabric/include/fabric2.hrl"). +-include_lib("fabric/test/fabric2_test.hrl"). + + +clean_old_indices_test_() -> + { + "Test cleanup of stale indices", + { + setup, + fun setup_all/0, + fun cleanup_all/1, + { + foreach, + fun setup/0, + fun cleanup/1, + [ + ?TDEF_FE(empty_db), + ?TDEF_FE(db_with_no_ddocs), + ?TDEF_FE(db_with_ddoc), + ?TDEF_FE(db_with_many_ddocs), + ?TDEF_FE(after_ddoc_deletion), + ?TDEF_FE(all_ddocs_deleted), + ?TDEF_FE(after_ddoc_recreated), + ?TDEF_FE(refcounted_sigs), + ?TDEF_FE(removes_old_jobs), + ?TDEF_FE(after_job_accepted_initial_build), + ?TDEF_FE(after_job_accepted_rebuild), + ?TDEF_FE(during_index_initial_build), + ?TDEF_FE(during_index_rebuild) + ] + } + } + }. + + +setup_all() -> + test_util:start_couch([ + fabric, + couch_jobs, + couch_js, + couch_views + ]). + + +cleanup_all(Ctx) -> + test_util:stop_couch(Ctx). + + +setup() -> + Opts = [{user_ctx, ?ADMIN_USER}], + {ok, Db} = fabric2_db:create(?tempdb(), Opts), + Db. + + +cleanup(Db) -> + meck:unload(), + ok = fabric2_db:delete(fabric2_db:name(Db), []). + + +empty_db(Db) -> + ?assertEqual(ok, fabric2_index:cleanup(Db)). + + +db_with_no_ddocs(Db) -> + create_docs(Db, 10), + ?assertEqual(ok, fabric2_index:cleanup(Db)). + + +db_with_ddoc(Db) -> + create_docs(Db, 10), + DDoc = create_ddoc(Db, <<"foo">>), + ?assertEqual(10, length(run_query(Db, DDoc))), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + ?assertEqual(10, length(run_query(Db, DDoc))). + + +db_with_many_ddocs(Db) -> + create_docs(Db, 10), + DDocs = create_ddocs(Db, 5), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs), + ?assertEqual(ok, fabric2_index:cleanup(Db)). + + +after_ddoc_deletion(Db) -> + create_docs(Db, 10), + DDocs = create_ddocs(Db, 2), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs), + [ToDel | RestDDocs] = DDocs, + delete_doc(Db, ToDel), + % Not yet cleaned up + ?assertEqual(true, view_has_data(Db, ToDel)), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + ?assertError({ddoc_deleted, _}, run_query(Db, ToDel)), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, RestDDocs). + + +all_ddocs_deleted(Db) -> + create_docs(Db, 10), + DDocs = create_ddocs(Db, 5), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs), + lists:foreach(fun(DDoc) -> + delete_doc(Db, DDoc) + end, DDocs), + % Not yet cleaned up + lists:foreach(fun(DDoc) -> + ?assertEqual(true, view_has_data(Db, DDoc)) + end, DDocs), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + lists:foreach(fun(DDoc) -> + ?assertError({ddoc_deleted, _}, run_query(Db, DDoc)) + end, DDocs). + + +after_ddoc_recreated(Db) -> + create_docs(Db, 10), + DDocs = create_ddocs(Db, 3), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs), + [ToDel | RestDDocs] = DDocs, + Deleted = delete_doc(Db, ToDel), + % Not yet cleaned up + ?assertEqual(true, view_has_data(Db, ToDel)), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + ?assertError({ddoc_deleted, _}, run_query(Db, ToDel)), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, RestDDocs), + recreate_doc(Db, Deleted), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + lists:foreach(fun(DDoc) -> + ?assertEqual(10, length(run_query(Db, DDoc))) + end, DDocs). + + +refcounted_sigs(Db) -> + create_docs(Db, 10), + DDoc1 = create_ddoc(Db, <<"1">>), + DDoc2 = create_doc(Db, <<"_design/2">>, DDoc1#doc.body), + ?assertEqual(10, length(run_query(Db, DDoc1))), + ?assertEqual(10, length(run_query(Db, DDoc2))), + + ?assertEqual(true, view_has_data(Db, DDoc1)), + ?assertEqual(true, view_has_data(Db, DDoc2)), + + delete_doc(Db, DDoc1), + ok = fabric2_index:cleanup(Db), + + ?assertEqual(true, view_has_data(Db, DDoc1)), + ?assertEqual(true, view_has_data(Db, DDoc2)), + + delete_doc(Db, DDoc2), + ok = fabric2_index:cleanup(Db), + + ?assertEqual(false, view_has_data(Db, DDoc1)), + ?assertEqual(false, view_has_data(Db, DDoc2)). + + +removes_old_jobs(Db) -> + create_docs(Db, 10), + DDoc = create_ddoc(Db, <<"foo">>), + + ?assertEqual(10, length(run_query(Db, DDoc))), + ?assertEqual(true, view_has_data(Db, DDoc)), + ?assertEqual(true, job_exists(Db, DDoc)), + + delete_doc(Db, DDoc), + ?assertEqual(ok, fabric2_index:cleanup(Db)), + + ?assertEqual(false, view_has_data(Db, DDoc)), + ?assertEqual(false, job_exists(Db, DDoc)). + + +after_job_accepted_initial_build(Db) -> + cleanup_during_initial_build(Db, fun meck_intercept_job_accept/2). + + +after_job_accepted_rebuild(Db) -> + cleanup_during_rebuild(Db, fun meck_intercept_job_accept/2). + + +during_index_initial_build(Db) -> + cleanup_during_initial_build(Db, fun meck_intercept_job_update/2). + + +during_index_rebuild(Db) -> + cleanup_during_rebuild(Db, fun meck_intercept_job_update/2). + + +cleanup_during_initial_build(Db, InterruptFun) -> + InterruptFun(fabric2_db:name(Db), self()), + + create_docs(Db, 10), + DDoc = create_ddoc(Db, <<"foo">>), + + {_, Ref1} = spawn_monitor(fun() -> run_query(Db, DDoc) end), + + receive {JobPid, triggered} -> ok end, + delete_doc(Db, DDoc), + ok = fabric2_index:cleanup(Db), + JobPid ! continue, + + receive {'DOWN', Ref1, _, _, _} -> ok end, + + ok = fabric2_index:cleanup(Db), + ?assertError({ddoc_deleted, _}, run_query(Db, DDoc)), + + ?assertEqual(false, view_has_data(Db, DDoc)), + ?assertEqual(false, job_exists(Db, DDoc)). + + +cleanup_during_rebuild(Db, InterruptFun) -> + create_docs(Db, 10), + DDoc = create_ddoc(Db, <<"foo">>), + ?assertEqual(10, length(run_query(Db, DDoc))), + + InterruptFun(fabric2_db:name(Db), self()), + + create_docs(Db, 10, 10), + + {_, Ref1} = spawn_monitor(fun() -> run_query(Db, DDoc) end), + + receive {JobPid, triggered} -> ok end, + delete_doc(Db, DDoc), + ok = fabric2_index:cleanup(Db), + JobPid ! continue, + + receive {'DOWN', Ref1, _, _, _} -> ok end, + + ok = fabric2_index:cleanup(Db), + ?assertError({ddoc_deleted, _}, run_query(Db, DDoc)), + + ?assertEqual(false, view_has_data(Db, DDoc)), + ?assertEqual(false, job_exists(Db, DDoc)). + + + +run_query(Db, DDocId) when is_binary(DDocId) -> + {ok, DDoc} = fabric2_db:open_doc(Db, <<"_design/", DDocId/binary>>), + run_query(Db, DDoc); + +run_query(Db, DDoc) -> + Fun = fun default_cb/2, + {ok, Result} = couch_views:query(Db, DDoc, <<"bar">>, Fun, [], #{}), + Result. + + +default_cb(complete, Acc) -> + {ok, lists:reverse(Acc)}; +default_cb({final, Info}, []) -> + {ok, [Info]}; +default_cb({final, _}, Acc) -> + {ok, Acc}; +default_cb({meta, _}, Acc) -> + {ok, Acc}; +default_cb(ok, ddoc_updated) -> + {ok, ddoc_updated}; +default_cb(Row, Acc) -> + {ok, [Row | Acc]}. + + +view_has_data(Db, DDoc) -> + DbName = fabric2_db:name(Db), + {ok, #mrst{sig = Sig}} = couch_views_util:ddoc_to_mrst(DbName, DDoc), + fabric2_fdb:transactional(Db, fun(TxDb) -> + #{ + tx := Tx, + db_prefix := DbPrefix + } = TxDb, + SigKeyTuple = {?DB_VIEWS, ?VIEW_INFO, ?VIEW_UPDATE_SEQ, Sig}, + SigKey = erlfdb_tuple:pack(SigKeyTuple, DbPrefix), + SigVal = erlfdb:wait(erlfdb:get(Tx, SigKey)), + + RangeKeyTuple = {?DB_VIEWS, ?VIEW_DATA, Sig}, + RangeKey = erlfdb_tuple:pack(RangeKeyTuple, DbPrefix), + Range = erlfdb:wait(erlfdb:get_range_startswith(Tx, RangeKey)), + + SigVal /= not_found andalso Range /= [] + end). + + +meck_intercept_job_accept(TgtDbName, ParentPid) -> + meck:new(fabric2_db, [passthrough]), + meck:expect(fabric2_db, open, fun + (DbName, Opts) when DbName == TgtDbName -> + Result = meck:passthrough([DbName, Opts]), + ParentPid ! {self(), triggered}, + receive continue -> ok end, + meck:unload(), + Result; + (DbName, Opts) -> + meck:passthrough([DbName, Opts]) + end). + + +meck_intercept_job_update(_DbName, ParentPid) -> + meck:new(couch_jobs, [passthrough]), + meck:expect(couch_jobs, finish, fun(Tx, Job, Data) -> + ParentPid ! {self(), triggered}, + receive continue -> ok end, + Result = meck:passthrough([Tx, Job, Data]), + meck:unload(), + Result + end). + + +create_ddoc(Db, Id) -> + MapFunFmt = "function(doc) {var f = \"~s\"; emit(doc.val, f)}", + MapFun = io_lib:format(MapFunFmt, [Id]), + Body = {[ + {<<"views">>, {[ + {<<"bar">>, {[{<<"map">>, iolist_to_binary(MapFun)}]}} + ]}} + ]}, + create_doc(Db, <<"_design/", Id/binary>>, Body). + + +recreate_doc(Db, #doc{deleted = true} = Doc) -> + #doc{ + id = DDocId, + body = Body + } = Doc, + create_doc(Db, DDocId, Body). + + +create_ddocs(Db, Count) when is_integer(Count), Count > 1 -> + lists:map(fun(Seq) -> + Id = io_lib:format("~6..0b", [Seq]), + create_ddoc(Db, iolist_to_binary(Id)) + end, lists:seq(1, Count)). + + +create_doc(Db, Id) -> + create_doc(Db, Id, {[{<<"value">>, Id}]}). + + +create_doc(Db, Id, Body) -> + Doc = #doc{ + id = Id, + body = Body + }, + {ok, {Pos, Rev}} = fabric2_db:update_doc(Db, Doc), + Doc#doc{revs = {Pos, [Rev]}}. + + +create_docs(Db, Count) -> + create_docs(Db, Count, 0). + + +create_docs(Db, Count, Offset) -> + lists:map(fun(Seq) -> + Id = io_lib:format("~6..0b", [Seq]), + create_doc(Db, iolist_to_binary(Id)) + end, lists:seq(Offset + 1, Offset + Count)). + + +delete_doc(Db, DDoc) -> + #doc{ + revs = {_, [_ | _] = Revs} + } = DDoc, + {ok, {NewPos, Rev}} = fabric2_db:update_doc(Db, DDoc#doc{deleted = true}), + DDoc#doc{ + revs = {NewPos, [Rev | Revs]}, + deleted = true + }. + + +job_exists(Db, DDoc) -> + JobId = job_id(Db, DDoc), + case couch_jobs:get_job_data(Db, ?INDEX_JOB_TYPE, JobId) of + {ok, _} -> true; + {error, not_found} -> false + end. + + +job_id(Db, DDoc) -> + DbName = fabric2_db:name(Db), + {ok, #mrst{sig = Sig}} = couch_views_util:ddoc_to_mrst(DbName, DDoc), + HexSig = fabric2_util:to_hex(Sig), + <<DbName/binary, "-", HexSig/binary>>. |