You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2017/06/19 21:59:29 UTC

[couchdb] 05/05: ss - Initial test suite check in

This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch optimize-ddoc-cache
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 54a048475ce5132e60fb2e043a6500ba70a89de7
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Mon Jun 19 16:58:36 2017 -0500

    ss - Initial test suite check in
    
    Will round out testing the cache entry refreshing tomorrow and then
    should end up with nearly 100% coverage.
---
 src/ddoc_cache/rebar.config                        |   2 +
 src/ddoc_cache/src/ddoc_cache.erl                  |   8 +-
 src/ddoc_cache/src/ddoc_cache_entry.erl            |  15 +-
 src/ddoc_cache/src/ddoc_cache_lru.erl              |  34 ++--
 src/ddoc_cache/src/ddoc_cache_opener.erl           |   2 +-
 src/ddoc_cache/src/ddoc_cache_sup.erl              |   2 +-
 src/ddoc_cache/src/ddoc_cache_tables.erl           |   7 +-
 src/ddoc_cache/test/ddoc_cache_basic_test.erl      | 111 +++++++++++++
 src/ddoc_cache/test/ddoc_cache_coverage_test.erl   |  74 +++++++++
 src/ddoc_cache/test/ddoc_cache_disabled_test.erl   |  52 ++++++
 src/ddoc_cache/test/ddoc_cache_ev.erl              |  22 +++
 src/ddoc_cache/test/ddoc_cache_eviction_test.erl   | 180 +++++++++++++++++++++
 src/ddoc_cache/test/ddoc_cache_no_cache_test.erl   |  75 +++++++++
 src/ddoc_cache/test/ddoc_cache_open_error_test.erl |  46 ++++++
 src/ddoc_cache/test/ddoc_cache_test.hrl            |  20 +++
 src/ddoc_cache/test/ddoc_cache_tutil.erl           |  70 ++++++++
 16 files changed, 687 insertions(+), 33 deletions(-)

diff --git a/src/ddoc_cache/rebar.config b/src/ddoc_cache/rebar.config
new file mode 100644
index 0000000..e0d1844
--- /dev/null
+++ b/src/ddoc_cache/rebar.config
@@ -0,0 +1,2 @@
+{cover_enabled, true}.
+{cover_print_enabled, true}.
diff --git a/src/ddoc_cache/src/ddoc_cache.erl b/src/ddoc_cache/src/ddoc_cache.erl
index f9eea9f..9100954 100644
--- a/src/ddoc_cache/src/ddoc_cache.erl
+++ b/src/ddoc_cache/src/ddoc_cache.erl
@@ -21,6 +21,7 @@
     open_doc/2,
     open_doc/3,
     open_validation_funs/1,
+    open_custom/2,
     evict/2,
 
     %% deprecated
@@ -54,9 +55,12 @@ open_custom(DbName, Mod) ->
     ddoc_cache_opener:open(Key).
 
 
-evict(ShardDbName, DDocIds) ->
+evict(ShardDbName, DDocIds) when is_list(DDocIds) ->
     DbName = mem3:dbname(ShardDbName),
-    ddoc_cache_lru:evict(DbName, DDocIds).
+    ddoc_cache_lru:evict(DbName, DDocIds);
+
+evict(ShardDbName, DDocId) ->
+    evict(ShardDbName, [DDocId]).
 
 
 open(DbName, validation_funs) ->
diff --git a/src/ddoc_cache/src/ddoc_cache_entry.erl b/src/ddoc_cache/src/ddoc_cache_entry.erl
index 80b8659..c19df90 100644
--- a/src/ddoc_cache/src/ddoc_cache_entry.erl
+++ b/src/ddoc_cache/src/ddoc_cache_entry.erl
@@ -18,8 +18,7 @@
     ddocid/1,
     spawn_link/1,
     handle_resp/1,
-    open/1,
-    recover/1
+    open/1
 ]).
 
 -export([
@@ -27,6 +26,7 @@
     do_open/2
 ]).
 
+
 dbname({Mod, Arg}) ->
     Mod:dbname(Arg).
 
@@ -36,17 +36,14 @@ ddocid({Mod, Arg}) ->
 
 
 spawn_link(Key) ->
-    erlang:spawn_link(?MODULE, do_open, [Key]).
+    erlang:spawn_link(?MODULE, do_open, [Key, true]).
 
 
 handle_resp({open_ok, _Key, Resp}) ->
     Resp;
 
 handle_resp({open_error, _Key, Type, Reason, Stack}) ->
-    erlang:raise(Type, Reason, Stack);
-
-handle_resp(Else) ->
-    erlang:error({ddoc_cache_entry, Else}).
+    erlang:raise(Type, Reason, Stack).
 
 
 open(Key) ->
@@ -57,10 +54,6 @@ open(Key) ->
     end.
 
 
-recover({Mod, Arg}) ->
-    Mod:recover(Arg).
-
-
 do_open(Key) ->
     do_open(Key, false).
 
diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl
index dbcaa28..fecf780 100644
--- a/src/ddoc_cache/src/ddoc_cache_lru.erl
+++ b/src/ddoc_cache/src/ddoc_cache_lru.erl
@@ -46,7 +46,6 @@
     keys, % key -> time
     dbs, % dbname -> docid -> key -> []
     time,
-    max_size,
     evictor
 }).
 
@@ -82,12 +81,10 @@ init(_) ->
     {ok, Evictor} = couch_event:link_listener(
             ?MODULE, handle_db_event, nil, [all_dbs]
         ),
-    MaxSize = config:get_integer("ddoc_cache", "max_size", 1000),
     {ok, #st{
         keys = Keys,
         dbs = Dbs,
         time = 0,
-        max_size = MaxSize,
         evictor = Evictor
     }}.
 
@@ -107,12 +104,14 @@ handle_call({insert, Key, Val}, _From, St) ->
         time = Time
     } = St,
     NewTime = Time + 1,
+    NewSt = St#st{time = NewTime},
     Pid = ddoc_cache_refresher:spawn(Key),
     true = ets:insert(?CACHE, #entry{key = Key, val = Val, pid = Pid}),
     true = ets:insert(?ATIMES, {NewTime, Key}),
-    ok = khash:put(Keys, NewTime),
+    ok = khash:put(Keys, Key, NewTime),
     store_key(Dbs, Key),
-    {reply, ok, trim(St#st{time = NewTime})};
+    trim(NewSt),
+    {reply, ok, NewSt};
 
 handle_call({refresh, Key, Val}, _From, St) ->
     #st{
@@ -168,7 +167,7 @@ handle_cast({accessed, Key}, St) ->
             true = is_process_alive(Pid),
             true = ets:delete(?ATIMES, OldTime),
             true = ets:insert(?ATIMES, {NewTime, Key}),
-            ok = khash:put(Keys, NewTime);
+            ok = khash:put(Keys, Key, NewTime);
         not_found ->
             % Likely a client read from the cache while an
             % eviction message was in our mailbox
@@ -176,12 +175,12 @@ handle_cast({accessed, Key}, St) ->
     end,
     {noreply, St};
 
-handle_cast({evict, _} = Msg, St) ->
-    gen_server:abcast(mem3:nodes(), ?MODULE, Msg),
+handle_cast({evict, DbName}, St) ->
+    gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName}),
     {noreply, St};
 
-handle_cast({evict, _, _} = Msg, St) ->
-    gen_server:abcast(mem3:nodes(), ?MODULE, Msg),
+handle_cast({evict, DbName, DDocIds}, St) ->
+    gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName, DDocIds}),
     {noreply, St};
 
 handle_cast({do_evict, DbName}, St) ->
@@ -201,9 +200,10 @@ handle_cast({do_evict, DbName}, St) ->
         not_found ->
             ok
     end,
+    ddoc_cache_ev:evicted(DbName, all),
     {noreply, St};
 
-handle_cast({do_evict, DbName, DDocIds}, St) ->
+handle_cast({do_evict, DbName, DDocIdList}, St) ->
     #st{
         dbs = Dbs
     } = St,
@@ -221,7 +221,7 @@ handle_cast({do_evict, DbName, DDocIds}, St) ->
                         ok
                 end,
                 khash:del(DDocIds, DDocId)
-            end, [no_ddocid | DDocIds]),
+            end, [no_ddocid | DDocIdList]),
             case khash:size(DDocIds) of
                 0 -> khash:del(Dbs, DbName);
                 _ -> ok
@@ -229,14 +229,14 @@ handle_cast({do_evict, DbName, DDocIds}, St) ->
         not_found ->
             ok
     end,
+    ddoc_cache_ev:evicted(DbName, DDocIdList),
     {noreply, St};
 
 handle_cast(Msg, St) ->
     {stop, {invalid_cast, Msg}, St}.
 
 
-handle_info({'EXIT', Pid, Reason}, #st{evictor=Pid}=St) ->
-    couch_log:error("ddoc_cache_opener evictor died ~w", [Reason]),
+handle_info({'EXIT', Pid, _Reason}, #st{evictor=Pid}=St) ->
     {ok, Evictor} = couch_event:link_listener(
             ?MODULE, handle_db_event, nil, [all_dbs]
         ),
@@ -277,15 +277,15 @@ store_key(Dbs, Key) ->
         not_found ->
             {ok, Keys} = khash:from_list([{Key, []}]),
             {ok, DDocIds} = khash:from_list([{DDocId, Keys}]),
-            khash:put(Dbs, DDocId, DDocIds)
+            khash:put(Dbs, DbName, DDocIds)
     end.
 
 
 trim(St) ->
     #st{
-        keys = Keys,
-        max_size = MaxSize
+        keys = Keys
     } = St,
+    MaxSize = config:get_integer("ddoc_cache", "max_size", 1000),
     case khash:size(Keys) > MaxSize of
         true ->
             case ets:first(?ATIMES) of
diff --git a/src/ddoc_cache/src/ddoc_cache_opener.erl b/src/ddoc_cache/src/ddoc_cache_opener.erl
index f6a4438..18524d3 100644
--- a/src/ddoc_cache/src/ddoc_cache_opener.erl
+++ b/src/ddoc_cache/src/ddoc_cache_opener.erl
@@ -106,7 +106,7 @@ handle_info({'EXIT', _Pid, {open_ok, OpenerKey, Resp}}, St) ->
     {noreply, St};
 
 handle_info({'EXIT', _Pid, {open_error, OpenerKey, Type, Reason, Stack}}, St) ->
-    respond(OpenerKey, {open_error, Type, Reason, Stack}),
+    respond(OpenerKey, {open_error, OpenerKey, Type, Reason, Stack}),
     {noreply, St};
 
 handle_info({'EXIT', Pid, Reason}, St) ->
diff --git a/src/ddoc_cache/src/ddoc_cache_sup.erl b/src/ddoc_cache/src/ddoc_cache_sup.erl
index ddb1232..35393e1 100644
--- a/src/ddoc_cache/src/ddoc_cache_sup.erl
+++ b/src/ddoc_cache/src/ddoc_cache_sup.erl
@@ -51,4 +51,4 @@ init([]) ->
             [ddoc_cache_opener]
         }
     ],
-    {ok, {{one_for_all, 5, 10}, Children}}.
+    {ok, {{one_for_all, 25, 1}, Children}}.
diff --git a/src/ddoc_cache/src/ddoc_cache_tables.erl b/src/ddoc_cache/src/ddoc_cache_tables.erl
index e097129..2d57163 100644
--- a/src/ddoc_cache/src/ddoc_cache_tables.erl
+++ b/src/ddoc_cache/src/ddoc_cache_tables.erl
@@ -38,7 +38,12 @@ start_link() ->
 
 init(_) ->
     BaseOpts = [public, named_table],
-    ets:new(?CACHE, [set, {read_concurrency, true}] ++ BaseOpts),
+    CacheOpts = [
+        set,
+        {read_concurrency, true},
+        {keypos, #entry.key}
+    ] ++ BaseOpts,
+    ets:new(?CACHE, CacheOpts),
     ets:new(?ATIMES, [ordered_set] ++ BaseOpts),
     ets:new(?OPENERS, [set, {keypos, #opener.key}] ++ BaseOpts),
     {ok, nil}.
diff --git a/src/ddoc_cache/test/ddoc_cache_basic_test.erl b/src/ddoc_cache/test/ddoc_cache_basic_test.erl
new file mode 100644
index 0000000..ea114bd
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_basic_test.erl
@@ -0,0 +1,111 @@
+% 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(ddoc_cache_basic_test).
+
+
+-export([
+    recover/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+recover(DbName) ->
+    {ok, {DbName, totes_custom}}.
+
+
+check_basic_test_() ->
+    {
+        setup,
+        fun ddoc_cache_tutil:start_couch/0,
+        fun ddoc_cache_tutil:stop_couch/1,
+        {with, [
+            fun cache_ddoc/1,
+            fun cache_ddoc_rev/1,
+            fun cache_vdu/1,
+            fun cache_custom/1,
+            fun cache_ddoc_refresher_unchanged/1,
+            fun dont_cache_not_found/1
+        ]}
+    }.
+
+
+cache_ddoc({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    Resp1 = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp1),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    Resp2 = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertEqual(Resp1, Resp2),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+cache_ddoc_rev({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    Resp1 = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp1),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    Resp2 = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    ?assertEqual(Resp1, Resp2),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+
+    % Assert that the non-rev cache entry is separate
+    Resp3 = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp3),
+    ?assertEqual(2, ets:info(?CACHE, size)).
+
+
+cache_vdu({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    Resp1 = ddoc_cache:open_validation_funs(DbName),
+    ?assertMatch({ok, [_]}, Resp1),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    Resp2 = ddoc_cache:open_validation_funs(DbName),
+    ?assertEqual(Resp1, Resp2),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+cache_custom({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    Resp1 = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertMatch({ok, {DbName, totes_custom}}, Resp1),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    Resp2 = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertEqual(Resp1, Resp2),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+cache_ddoc_refresher_unchanged({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    ddoc_cache:open_doc(DbName, ?FOOBAR),
+    [Entry1] = ets:lookup(?CACHE, ets:first(?CACHE)),
+    ddoc_cache:open_doc(DbName, ?FOOBAR),
+    [Entry2] = ets:lookup(?CACHE, ets:first(?CACHE)),
+    ?assertEqual(Entry1, Entry2).
+
+
+dont_cache_not_found({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Resp = ddoc_cache:open_doc(DbName, <<"_design/not_found">>),
+    ?assertEqual({not_found, missing}, Resp),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    ?assertEqual(0, ets:info(?ATIMES, size)).
diff --git a/src/ddoc_cache/test/ddoc_cache_coverage_test.erl b/src/ddoc_cache/test/ddoc_cache_coverage_test.erl
new file mode 100644
index 0000000..0cc4cdf
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_coverage_test.erl
@@ -0,0 +1,74 @@
+% 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(ddoc_cache_coverage_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+check_basic_test_() ->
+    {
+        setup,
+        fun ddoc_cache_tutil:start_couch/0,
+        fun ddoc_cache_tutil:stop_couch/1,
+        [
+            fun restart_opener/0,
+            fun restart_lru/0,
+            fun restart_tables/0
+        ]
+    }.
+
+
+restart_opener() ->
+    send_bad_messages(ddoc_cache_opener),
+    wait_for_restart(ddoc_cache_opener, fun() ->
+        whereis(ddoc_cache_opener) ! {'EXIT', a_pid, because}
+    end),
+    ?assertEqual(ok, ddoc_cache_opener:terminate(bang, bable)),
+    ?assertEqual({ok, foo}, ddoc_cache_opener:code_change(1, foo, [])).
+
+
+restart_lru() ->
+    send_bad_messages(ddoc_cache_lru),
+    ?assertEqual(ok, ddoc_cache_lru:terminate(bang, {st, a, b, c, d})),
+    ?assertEqual({ok, foo}, ddoc_cache_lru:code_change(1, foo, [])).
+
+
+restart_tables() ->
+    send_bad_messages(ddoc_cache_tables),
+    ?assertEqual(ok, ddoc_cache_tables:terminate(bang, baz)),
+    ?assertEqual({ok, foo}, ddoc_cache_tables:code_change(1, foo, [])).
+
+
+send_bad_messages(Name) ->
+    wait_for_restart(Name, fun() ->
+        ?assertEqual({invalid_call, foo}, gen_server:call(Name, foo))
+    end),
+    wait_for_restart(Name, fun() ->
+        gen_server:cast(Name, foo)
+    end),
+    wait_for_restart(Name, fun() ->
+        whereis(Name) ! foo
+    end).
+
+
+wait_for_restart(Server, Fun) ->
+    Ref = erlang:monitor(process, whereis(Server)),
+    Fun(),
+    receive
+        {'DOWN', Ref, _, _, _} ->
+            ok
+    end,
+    ?assert(is_pid(test_util:wait_process(Server))).
diff --git a/src/ddoc_cache/test/ddoc_cache_disabled_test.erl b/src/ddoc_cache/test/ddoc_cache_disabled_test.erl
new file mode 100644
index 0000000..3e412c7
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_disabled_test.erl
@@ -0,0 +1,52 @@
+% 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(ddoc_cache_disabled_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+start_couch() ->
+    Ctx = ddoc_cache_tutil:start_couch(),
+    config:set("ddoc_cache", "max_size", "0", false),
+    Ctx.
+
+
+check_disabled_test_() ->
+    {
+        setup,
+        fun start_couch/0,
+        fun ddoc_cache_tutil:stop_couch/1,
+        {with, [
+            fun resp_ok/1,
+            fun resp_not_found/1
+        ]}
+    }.
+
+
+resp_ok({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Resp = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertMatch({ok, #doc{id = ?FOOBAR}}, Resp),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    ?assertEqual(0, ets:info(?ATIMES, size)).
+
+
+resp_not_found({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Resp = ddoc_cache:open_doc(DbName, <<"_design/not_found">>),
+    ?assertEqual({not_found, missing}, Resp),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+    ?assertEqual(0, ets:info(?ATIMES, size)).
diff --git a/src/ddoc_cache/test/ddoc_cache_ev.erl b/src/ddoc_cache/test/ddoc_cache_ev.erl
new file mode 100644
index 0000000..a9ea475
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_ev.erl
@@ -0,0 +1,22 @@
+% 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(ddoc_cache_ev).
+
+-export([
+    evicted/2
+]).
+
+
+evicted(_DbName, _DDocIds) ->
+    couch_log:error("EVICTION EVENT: ~p ~p~n", [_DbName, _DDocIds]),
+    ok.
diff --git a/src/ddoc_cache/test/ddoc_cache_eviction_test.erl b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl
new file mode 100644
index 0000000..d36157d
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl
@@ -0,0 +1,180 @@
+% 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(ddoc_cache_eviction_test).
+
+
+-export([
+    recover/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+recover(DbName) ->
+    {ok, {DbName, totes_custom}}.
+
+
+start_couch() ->
+    Ctx = ddoc_cache_tutil:start_couch(),
+    meck:new(ddoc_cache_ev, [passthrough]),
+    Ctx.
+
+
+stop_couch(Ctx) ->
+    meck:unload(ddoc_cache_ev),
+    ddoc_cache_tutil:stop_couch(Ctx).
+
+
+check_eviction_test_() ->
+    {
+        setup,
+        fun start_couch/0,
+        fun stop_couch/1,
+        {with, [
+            fun evict_ddoc/1,
+            fun evict_ddoc_rev/1,
+            fun evict_vdu/1,
+            fun evict_custom/1,
+            fun evict_vdu_unrelated/1,
+            fun evict_custom_unrelated/1,
+            fun evict_all/1,
+            fun dont_evict_unrelated/1,
+            fun dont_evict_all_unrelated/1,
+            fun check_upgrade_clauses/1
+        ]}
+    }.
+
+
+evict_ddoc({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, [?FOOBAR]),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_ddoc_rev({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, [?FOOBAR]),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_vdu({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_validation_funs(DbName),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, ?VDU),
+    wait_for_eviction(DbName, [?VDU]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_custom({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, ?CUSTOM),
+    wait_for_eviction(DbName, [?CUSTOM]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_vdu_unrelated({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_validation_funs(DbName),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, [?FOOBAR]),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_custom_unrelated({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, [?FOOBAR]),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+evict_all({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    ShardName = element(2, hd(mem3:shards(DbName))),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    {ok, _} = ddoc_cache:open_validation_funs(DbName),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertEqual(4, ets:info(?CACHE, size)),
+    {ok, _} = ddoc_cache_lru:handle_db_event(ShardName, deleted, foo),
+    wait_for_eviction(DbName, all),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+dont_evict_unrelated({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?VDU),
+    ?assertEqual(2, ets:info(?CACHE, size)),
+    ddoc_cache:evict(DbName, [?FOOBAR]),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+dont_evict_all_unrelated({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    {ok, _} = ddoc_cache:open_validation_funs(DbName),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    ?assertEqual(4, ets:info(?CACHE, size)),
+    ShardName = <<"shards/00000000-ffffffff/test.1384769918">>,
+    {ok, _} = ddoc_cache_lru:handle_db_event(ShardName, deleted, foo),
+    wait_for_eviction(<<"test">>, all),
+    ?assertEqual(4, ets:info(?CACHE, size)).
+
+
+check_upgrade_clauses({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    ShardName = element(2, hd(mem3:shards(DbName))),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    gen_server:cast(ddoc_cache_opener, {do_evict, DbName}),
+    wait_for_eviction(DbName, all),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+
+    {ok, _} = ddoc_cache:open_doc(DbName, ?VDU),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    gen_server:cast(ddoc_cache_opener, {do_evict, DbName, [?VDU]}),
+    wait_for_eviction(DbName, [?VDU]),
+    ?assertEqual(0, ets:info(?CACHE, size)),
+
+    % Make sure evict all vs specific doc ids works
+    {ok, _} = ddoc_cache:open_doc(DbName, ?CUSTOM),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    gen_server:cast(ddoc_cache_opener, {do_evict, DbName, [?FOOBAR]}),
+    wait_for_eviction(DbName, [?FOOBAR]),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+
+wait_for_eviction(DbName, DDocIds) ->
+    meck:reset(ddoc_cache_ev),
+    meck:wait(ddoc_cache_ev, evicted, [DbName, DDocIds], 1000).
diff --git a/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl b/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl
new file mode 100644
index 0000000..43891c8
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_no_cache_test.erl
@@ -0,0 +1,75 @@
+% 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(ddoc_cache_no_cache_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+ddoc(DDocId) ->
+    {ok, #doc{
+        id = DDocId,
+        body = {[
+            {<<"ohai">>, null}
+        ]}
+    }}.
+
+
+not_found(_DDocId) ->
+    {not_found, missing}.
+
+
+return_error(_DDocId) ->
+    {error, timeout}.
+
+
+start(Resp) ->
+    meck:new(fabric),
+    meck:expect(fabric, open_doc, fun(_, DDocId, _) ->
+        Resp(DDocId)
+    end).
+
+
+stop() ->
+    meck:unload().
+
+
+no_cache_open_ok_test() ->
+    start(fun ddoc/1),
+    try
+        Resp = ddoc_cache:open_doc(<<"foo">>, <<"bar">>),
+        ?assertEqual(ddoc(<<"bar">>), Resp)
+    after
+        stop()
+    end.
+
+
+no_cache_open_not_found_test() ->
+    start(fun not_found/1),
+    try
+        Resp = ddoc_cache:open_doc(<<"foo">>, <<"bar">>),
+        ?assertEqual(not_found(<<"bar">>), Resp)
+    after
+        stop()
+    end.
+
+
+no_cache_open_error_test() ->
+    start(fun return_error/1),
+    try
+        Resp = ddoc_cache:open_doc(<<"foo">>, <<"bar">>),
+        ?assertEqual(return_error(<<"bar">>), Resp)
+    after
+        stop()
+    end.
diff --git a/src/ddoc_cache/test/ddoc_cache_open_error_test.erl b/src/ddoc_cache/test/ddoc_cache_open_error_test.erl
new file mode 100644
index 0000000..0ac2390
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_open_error_test.erl
@@ -0,0 +1,46 @@
+% 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(ddoc_cache_open_error_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+start_couch() ->
+    Ctx = ddoc_cache_tutil:start_couch(),
+    meck:expect(fabric, open_doc, fun(_, ?FOOBAR, _) ->
+        erlang:error(test_kaboom)
+    end),
+    Ctx.
+
+
+stop_couch(Ctx) ->
+    meck:unload(),
+    ddoc_cache_tutil:stop_couch(Ctx).
+
+
+check_basic_test_() ->
+    {
+        setup,
+        fun start_couch/0,
+        fun stop_couch/1,
+        {with, [
+            fun handle_open_error/1
+        ]}
+    }.
+
+
+handle_open_error({DbName, _}) ->
+    ?assertError(test_kaboom, ddoc_cache:open_doc(DbName, ?FOOBAR)).
diff --git a/src/ddoc_cache/test/ddoc_cache_test.hrl b/src/ddoc_cache/test/ddoc_cache_test.hrl
new file mode 100644
index 0000000..1a09804
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_test.hrl
@@ -0,0 +1,20 @@
+% 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.
+
+
+-define(CACHE, ddoc_cache_entries).
+-define(ATIMES, ddoc_cache_atimes).
+-define(OPENERS, ddoc_cache_openers).
+
+-define(FOOBAR, <<"_design/foobar">>).
+-define(VDU, <<"_design/vdu">>).
+-define(CUSTOM, <<"_design/custom">>).
\ No newline at end of file
diff --git a/src/ddoc_cache/test/ddoc_cache_tutil.erl b/src/ddoc_cache/test/ddoc_cache_tutil.erl
new file mode 100644
index 0000000..0ac68fa
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_tutil.erl
@@ -0,0 +1,70 @@
+% 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(ddoc_cache_tutil).
+
+
+-compile(export_all).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+
+
+start_couch() ->
+    Ctx = test_util:start_couch(?CONFIG_CHAIN, [chttpd, ddoc_cache]),
+    TmpDb = ?tempdb(),
+    ok = fabric:create_db(TmpDb, [{q, "1"}, {n, "1"}]),
+    {ok, _} = fabric:update_docs(TmpDb, ddocs(), [?ADMIN_CTX]),
+    {TmpDb, Ctx}.
+
+
+stop_couch({_TmpDb, Ctx}) ->
+    test_util:stop_couch(Ctx).
+
+
+clear() ->
+    application:stop(ddoc_cache),
+    application:start(ddoc_cache).
+
+
+get_rev(DbName, DDocId) ->
+    {_, Ref} = erlang:spawn_monitor(fun() ->
+        {ok, #doc{revs = Revs}} = fabric:open_doc(DbName, DDocId, [?ADMIN_CTX]),
+        {Depth, [RevId]} = Revs,
+        exit({Depth, RevId})
+    end),
+    receive
+        {'DOWN', Ref, _, _, Rev} -> Rev
+    end.
+
+
+ddocs() ->
+    FooBar = #doc{
+        id = <<"_design/foobar">>,
+        body = {[
+            {<<"foo">>, <<"bar">>}
+        ]}
+    },
+    VDU = #doc{
+        id = <<"_design/vdu">>,
+        body = {[
+            {<<"validate_doc_update">>, <<"function(doc) {return;}">>}
+        ]}
+    },
+    Custom = #doc{
+        id = <<"_design/custom">>,
+        body = {[
+            {<<"custom">>, <<"hotrod">>}
+        ]}
+    },
+    [FooBar, VDU, Custom].

-- 
To stop receiving notification emails like this one, please contact
"commits@couchdb.apache.org" <co...@couchdb.apache.org>.