couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dav...@apache.org
Subject [couchdb] 02/02: ss - test suite boom
Date Tue, 20 Jun 2017 22:33:28 GMT
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 bba3df87902693682598364aa595628ce8f23e49
Author: Paul J. Davis <paul.joseph.davis@gmail.com>
AuthorDate: Tue Jun 20 17:33:13 2017 -0500

    ss - test suite boom
---
 src/couch/src/couch_db_updater.erl                |   2 +-
 src/ddoc_cache/src/ddoc_cache.app.src             |   7 -
 src/ddoc_cache/src/ddoc_cache.erl                 |  19 +-
 src/ddoc_cache/src/ddoc_cache.hrl                 |  11 +-
 src/ddoc_cache/src/ddoc_cache_entry.erl           |  29 +--
 src/ddoc_cache/src/ddoc_cache_lru.erl             | 141 +++++++--------
 src/ddoc_cache/src/ddoc_cache_opener.erl          |  33 ++--
 src/ddoc_cache/src/ddoc_cache_refresher.erl       | 151 +++++++---------
 src/ddoc_cache/src/ddoc_cache_tables.erl          |   2 +-
 src/ddoc_cache/src/ddoc_cache_util.erl            |  34 ----
 src/ddoc_cache/test/ddoc_cache_basic_test.erl     |  14 +-
 src/ddoc_cache/test/ddoc_cache_coverage_test.erl  |  36 +++-
 src/ddoc_cache/test/ddoc_cache_disabled_test.erl  |   4 +-
 src/ddoc_cache/test/ddoc_cache_ev.erl             |   7 +-
 src/ddoc_cache/test/ddoc_cache_eviction_test.erl  | 107 +----------
 src/ddoc_cache/test/ddoc_cache_opener_test.erl    |  76 ++++++++
 src/ddoc_cache/test/ddoc_cache_refresh_test.erl   | 167 ++++++++++++++++++
 src/ddoc_cache/test/ddoc_cache_refresher_test.erl | 164 +++++++++++++++++
 src/ddoc_cache/test/ddoc_cache_remove_test.erl    | 205 ++++++++++++++++++++++
 src/ddoc_cache/test/ddoc_cache_test.hrl           |  10 +-
 src/ddoc_cache/test/ddoc_cache_tutil.erl          |   3 +-
 21 files changed, 867 insertions(+), 355 deletions(-)

diff --git a/src/couch/src/couch_db_updater.erl b/src/couch/src/couch_db_updater.erl
index 2b448fd..1c4b561 100644
--- a/src/couch/src/couch_db_updater.erl
+++ b/src/couch/src/couch_db_updater.erl
@@ -319,7 +319,7 @@ handle_info({update_docs, Client, GroupedDocs, NonRepDocs, MergeConflicts,
                     couch_event:notify(Db2#db.name, {ddoc_updated, DDocId})
                 end, UpdatedDDocIds),
                 couch_event:notify(Db2#db.name, ddoc_updated),
-                ddoc_cache:evict(Db2#db.name, UpdatedDDocIds),
+                ddoc_cache:refresh(Db2#db.name, UpdatedDDocIds),
                 refresh_validate_doc_funs(Db2);
             false ->
                 Db2
diff --git a/src/ddoc_cache/src/ddoc_cache.app.src b/src/ddoc_cache/src/ddoc_cache.app.src
index 05c5b16..38fceda 100644
--- a/src/ddoc_cache/src/ddoc_cache.app.src
+++ b/src/ddoc_cache/src/ddoc_cache.app.src
@@ -13,13 +13,6 @@
 {application, ddoc_cache, [
     {description, "Design Document Cache"},
     {vsn, git},
-    {modules, [
-        ddoc_cache,
-        ddoc_cache_app,
-        ddoc_cache_opener,
-        ddoc_cache_sup,
-        ddoc_cache_util
-    ]},
     {registered, [
         ddoc_cache_tables,
         ddoc_cache_lru,
diff --git a/src/ddoc_cache/src/ddoc_cache.erl b/src/ddoc_cache/src/ddoc_cache.erl
index 9100954..ff87258 100644
--- a/src/ddoc_cache/src/ddoc_cache.erl
+++ b/src/ddoc_cache/src/ddoc_cache.erl
@@ -12,28 +12,18 @@
 
 -module(ddoc_cache).
 
--export([
-    start/0,
-    stop/0
-]).
 
 -export([
     open_doc/2,
     open_doc/3,
     open_validation_funs/1,
     open_custom/2,
-    evict/2,
+    refresh/2,
 
     %% deprecated
     open/2
 ]).
 
-start() ->
-    application:start(ddoc_cache).
-
-stop() ->
-    application:stop(ddoc_cache).
-
 
 open_doc(DbName, DocId) ->
     Key = {ddoc_cache_entry_ddocid, {DbName, DocId}},
@@ -55,12 +45,9 @@ open_custom(DbName, Mod) ->
     ddoc_cache_opener:open(Key).
 
 
-evict(ShardDbName, DDocIds) when is_list(DDocIds) ->
+refresh(ShardDbName, DDocIds) when is_list(DDocIds) ->
     DbName = mem3:dbname(ShardDbName),
-    ddoc_cache_lru:evict(DbName, DDocIds);
-
-evict(ShardDbName, DDocId) ->
-    evict(ShardDbName, [DDocId]).
+    ddoc_cache_lru:refresh(DbName, DDocIds).
 
 
 open(DbName, validation_funs) ->
diff --git a/src/ddoc_cache/src/ddoc_cache.hrl b/src/ddoc_cache/src/ddoc_cache.hrl
index 6986211..e11b350 100644
--- a/src/ddoc_cache/src/ddoc_cache.hrl
+++ b/src/ddoc_cache/src/ddoc_cache.hrl
@@ -16,9 +16,9 @@
 -type revision() :: {pos_integer(), doc_hash()}.
 
 -define(CACHE, ddoc_cache_entries).
--define(ATIMES, ddoc_cache_atimes).
+-define(LRU, ddoc_cache_lru).
 -define(OPENERS, ddoc_cache_openers).
-
+-define(REFRESH_TIMEOUT, 67000).
 
 -record(entry, {
     key,
@@ -31,3 +31,10 @@
     pid,
     clients
 }).
+
+
+-ifdef(TEST).
+-define(EVENT(Name, Arg), ddoc_cache_ev:event(Name, Arg)).
+-else.
+-define(EVENT(Name, Arg), ignore).
+-endif.
diff --git a/src/ddoc_cache/src/ddoc_cache_entry.erl b/src/ddoc_cache/src/ddoc_cache_entry.erl
index c19df90..33f2b4c 100644
--- a/src/ddoc_cache/src/ddoc_cache_entry.erl
+++ b/src/ddoc_cache/src/ddoc_cache_entry.erl
@@ -14,15 +14,17 @@
 
 
 -export([
-    dbname/1,
-    ddocid/1,
-    spawn_link/1,
+    spawn_opener/1,
+    spawn_refresher/1,
+
+    open/1,
     handle_resp/1,
-    open/1
+
+    dbname/1,
+    ddocid/1
 ]).
 
 -export([
-    do_open/1,
     do_open/2
 ]).
 
@@ -35,29 +37,32 @@ ddocid({Mod, Arg}) ->
     Mod:ddocid(Arg).
 
 
-spawn_link(Key) ->
+spawn_opener(Key) ->
     erlang:spawn_link(?MODULE, do_open, [Key, true]).
 
 
+spawn_refresher(Key) ->
+    erlang:spawn_monitor(?MODULE, do_open, [Key, false]).
+
+
 handle_resp({open_ok, _Key, Resp}) ->
     Resp;
 
 handle_resp({open_error, _Key, Type, Reason, Stack}) ->
-    erlang:raise(Type, Reason, Stack).
+    erlang:raise(Type, Reason, Stack);
+
+handle_resp(Other) ->
+    erlang:error({ddoc_cache_error, Other}).
 
 
 open(Key) ->
-    {_Pid, Ref} = erlang:spawn_monitor(?MODULE, do_open, [Key]),
+    {_Pid, Ref} = erlang:spawn_monitor(?MODULE, do_open, [Key, false]),
     receive
         {'DOWN', Ref, _, _, Resp} ->
             handle_resp(Resp)
     end.
 
 
-do_open(Key) ->
-    do_open(Key, false).
-
-
 do_open({Mod, Arg} = Key, DoInsert) ->
     try Mod:recover(Arg) of
         {ok, Resp} when DoInsert ->
diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl
index 2dd680c..b986b07 100644
--- a/src/ddoc_cache/src/ddoc_cache_lru.erl
+++ b/src/ddoc_cache/src/ddoc_cache_lru.erl
@@ -20,9 +20,9 @@
 
     insert/2,
     accessed/1,
-    refresh/2,
+    update/2,
     remove/1,
-    evict/2
+    refresh/2
 ]).
 
 -export([
@@ -43,20 +43,13 @@
 
 
 -record(st, {
-    keys, % key -> time
+    atimes, % key -> time
     dbs, % dbname -> docid -> key -> []
     time,
     evictor
 }).
 
 
--ifdef(TEST).
--define(EVICTED(DbName, DDocIds), ddoc_cache_ev:evicted(DbName, DDocIds)).
--else.
--define(EVICTED(DbName, DDocIds), ignore).
--endif.
-
-
 start_link() ->
     gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
 
@@ -69,27 +62,27 @@ accessed(Key) ->
     gen_server:cast(?MODULE, {accessed, Key}).
 
 
-refresh(Key, Val) ->
-    gen_server:call(?MODULE, {refresh, Key, Val}).
+update(Key, Val) ->
+    gen_server:call(?MODULE, {update, Key, Val}).
 
 
 remove(Key) ->
     gen_server:call(?MODULE, {remove, Key}).
 
 
--spec evict(dbname(), [docid()]) -> ok.
-evict(DbName, DDocIds) ->
-    gen_server:cast(?MODULE, {evict, DbName, DDocIds}).
+refresh(DbName, DDocIds) ->
+    gen_server:cast(?MODULE, {refresh, DbName, DDocIds}).
 
 
 init(_) ->
-    {ok, Keys} = khash:new(),
+    process_flag(trap_exit, true),
+    {ok, ATimes} = khash:new(),
     {ok, Dbs} = khash:new(),
     {ok, Evictor} = couch_event:link_listener(
             ?MODULE, handle_db_event, nil, [all_dbs]
         ),
     {ok, #st{
-        keys = Keys,
+        atimes = ATimes,
         dbs = Dbs,
         time = 0,
         evictor = Evictor
@@ -106,27 +99,29 @@ terminate(_Reason, St) ->
 
 handle_call({insert, Key, Val}, _From, St) ->
     #st{
-        keys = Keys,
+        atimes = ATimes,
         dbs = Dbs,
         time = Time
     } = St,
     NewTime = Time + 1,
     NewSt = St#st{time = NewTime},
-    Pid = ddoc_cache_refresher:spawn(Key),
+    Pid = ddoc_cache_refresher:spawn_link(Key, ?REFRESH_TIMEOUT),
     true = ets:insert(?CACHE, #entry{key = Key, val = Val, pid = Pid}),
-    true = ets:insert(?ATIMES, {NewTime, Key}),
-    ok = khash:put(Keys, Key, NewTime),
+    true = ets:insert(?LRU, {NewTime, Key}),
+    ok = khash:put(ATimes, Key, NewTime),
     store_key(Dbs, Key),
     trim(NewSt),
+    ?EVENT(inserted, {Key, Val}),
     {reply, ok, NewSt};
 
-handle_call({refresh, Key, Val}, _From, St) ->
+handle_call({update, Key, Val}, _From, St) ->
     #st{
-        keys = Keys
+        atimes = ATimes
     } = St,
-    case khash:lookup(Keys, Key) of
+    case khash:lookup(ATimes, Key) of
         {value, _} ->
             ets:update_element(?CACHE, Key, {#entry.val, Val}),
+            ?EVENT(updated, {Key, Val}),
             {reply, ok, St};
         not_found ->
             {reply, evicted, St}
@@ -134,12 +129,15 @@ handle_call({refresh, Key, Val}, _From, St) ->
 
 handle_call({remove, Key}, _From, St) ->
     #st{
-        keys = TimeKeys,
+        atimes = ATimes,
         dbs = Dbs
     } = St,
-    case khash:lookup(TimeKeys, Key) of
+    case khash:lookup(ATimes, Key) of
         {value, ATime} ->
-            remove_entry(St, Key, ATime),
+            [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
+            ddoc_cache_refresher:stop(Pid),
+            remove_key(St, Key, ATime),
+
             DbName = ddoc_cache_entry:dbname(Key),
             DDocId = ddoc_cache_entry:ddocid(Key),
             {value, DDocIds} = khash:lookup(Dbs, DbName),
@@ -152,7 +150,9 @@ handle_call({remove, Key}, _From, St) ->
             case khash:size(DDocIds) of
                 0 -> khash:del(Dbs, DDocId);
                 _ -> ok
-            end;
+            end,
+
+            ?EVENT(removed, Key);
         not_found ->
             ok
     end,
@@ -164,17 +164,18 @@ handle_call(Msg, _From, St) ->
 
 handle_cast({accessed, Key}, St) ->
     #st{
-        keys = Keys,
+        atimes = ATimes,
         time = Time
     } = St,
     NewTime = Time + 1,
-    case khash:lookup(Keys, Key) of
+    case khash:lookup(ATimes, Key) of
         {value, OldTime} ->
             [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
             true = is_process_alive(Pid),
-            true = ets:delete(?ATIMES, OldTime),
-            true = ets:insert(?ATIMES, {NewTime, Key}),
-            ok = khash:put(Keys, Key, NewTime);
+            true = ets:delete(?LRU, OldTime),
+            true = ets:insert(?LRU, {NewTime, Key}),
+            ok = khash:put(ATimes, Key, NewTime),
+            ?EVENT(accessed, Key);
         not_found ->
             % Likely a client read from the cache while an
             % eviction message was in our mailbox
@@ -186,8 +187,8 @@ handle_cast({evict, DbName}, St) ->
     gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName}),
     {noreply, St};
 
-handle_cast({evict, DbName, DDocIds}, St) ->
-    gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName, DDocIds}),
+handle_cast({refresh, DbName, DDocIds}, St) ->
+    gen_server:abcast(mem3:nodes(), ?MODULE, {do_refresh, DbName, DDocIds}),
     {noreply, St};
 
 handle_cast({do_evict, DbName}, St) ->
@@ -200,17 +201,18 @@ handle_cast({do_evict, DbName}, St) ->
                 khash:fold(Keys, fun(Key, _, _) ->
                     [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
                     ddoc_cache_refresher:stop(Pid),
-                    remove_entry(St, Key)
+                    remove_key(St, Key)
                 end, nil)
             end, nil),
-            khash:del(Dbs, DbName);
+            khash:del(Dbs, DbName),
+            ?EVENT(evicted, DbName);
         not_found ->
+            ?EVENT(evict_noop, DbName),
             ok
     end,
-    ?EVICTED(DbName, all),
     {noreply, St};
 
-handle_cast({do_evict, DbName, DDocIdList}, St) ->
+handle_cast({do_refresh, DbName, DDocIdList}, St) ->
     #st{
         dbs = Dbs
     } = St,
@@ -221,29 +223,23 @@ handle_cast({do_evict, DbName, DDocIdList}, St) ->
                     {value, Keys} ->
                         khash:fold(Keys, fun(Key, _, _) ->
                             [#entry{pid = Pid}] = ets:lookup(?CACHE, Key),
-                            ddoc_cache_refresher:stop(Pid),
-                            remove_entry(St, Key)
+                            ddoc_cache_refresher:refresh(Pid)
                         end, nil);
                     not_found ->
                         ok
-                end,
-                khash:del(DDocIds, DDocId)
-            end, [no_ddocid | DDocIdList]),
-            case khash:size(DDocIds) of
-                0 -> khash:del(Dbs, DbName);
-                _ -> ok
-            end;
+                end
+            end, [no_ddocid | DDocIdList]);
         not_found ->
             ok
     end,
-    ?EVICTED(DbName, DDocIdList),
     {noreply, St};
 
 handle_cast(Msg, St) ->
     {stop, {invalid_cast, Msg}, St}.
 
 
-handle_info({'EXIT', Pid, _Reason}, #st{evictor=Pid}=St) ->
+handle_info({'EXIT', Pid, _Reason}, #st{evictor = Pid} = St) ->
+    ?EVENT(evictor_died, Pid),
     {ok, Evictor} = couch_event:link_listener(
             ?MODULE, handle_db_event, nil, [all_dbs]
         ),
@@ -288,38 +284,33 @@ store_key(Dbs, Key) ->
     end.
 
 
-trim(St) ->
+remove_key(St, Key) ->
     #st{
-        keys = Keys
+        atimes = ATimes
     } = St,
-    MaxSize = config:get_integer("ddoc_cache", "max_size", 1000),
-    case khash:size(Keys) > MaxSize of
-        true ->
-            case ets:first(?ATIMES) of
-                '$end_of_table' ->
-                    ok;
-                ATime ->
-                    [{ATime, Key}] = ets:lookup(?ATIMES, ATime),
-                    remove_entry(St, Key, ATime),
-                    trim(St)
-            end;
-        false ->
-            ok
-    end.
+    {value, ATime} = khash:lookup(ATimes, Key),
+    remove_key(St, Key, ATime).
 
 
-remove_entry(St, Key) ->
+remove_key(St, Key, ATime) ->
     #st{
-        keys = Keys
+        atimes = ATimes
     } = St,
-    {value, ATime} = khash:lookup(Keys, Key),
-    remove_entry(St, Key, ATime).
+    true = ets:delete(?CACHE, Key),
+    true = ets:delete(?LRU, ATime),
+    ok = khash:del(ATimes, Key).
 
 
-remove_entry(St, Key, ATime) ->
+trim(St) ->
     #st{
-        keys = Keys
+        atimes = ATimes
     } = St,
-    true = ets:delete(?CACHE, Key),
-    true = ets:delete(?ATIMES, ATime),
-    ok = khash:del(Keys, Key).
+    MaxSize = max(0, config:get_integer("ddoc_cache", "max_size", 1000)),
+    case khash:size(ATimes) > MaxSize of
+        true ->
+            [{ATime, Key}] = ets:lookup(?LRU, ets:first(?LRU)),
+            remove_key(St, Key, ATime),
+            trim(St);
+        false ->
+            ok
+    end.
diff --git a/src/ddoc_cache/src/ddoc_cache_opener.erl b/src/ddoc_cache/src/ddoc_cache_opener.erl
index 18524d3..8368f82 100644
--- a/src/ddoc_cache/src/ddoc_cache_opener.erl
+++ b/src/ddoc_cache/src/ddoc_cache_opener.erl
@@ -38,9 +38,6 @@
 -include("ddoc_cache.hrl").
 
 
--define(LRU, ddoc_cache_lru).
-
-
 -record(st, {
     db_ddocs
 }).
@@ -73,14 +70,24 @@ terminate(_Reason, _St) ->
     ok.
 
 handle_call({open, OpenerKey}, From, St) ->
-    case ets:lookup(?OPENERS, OpenerKey) of
-        [#opener{clients=Clients}=O] ->
-            ets:insert(?OPENERS, O#opener{clients=[From | Clients]}),
-            {noreply, St};
+    case ets:lookup(?CACHE, OpenerKey) of
         [] ->
-            Pid = ddoc_cache_entry:spawn_link(OpenerKey),
-            ets:insert(?OPENERS, #opener{key=OpenerKey, pid=Pid, clients=[From]}),
-            {noreply, St}
+            case ets:lookup(?OPENERS, OpenerKey) of
+                [#opener{clients=Clients}=O] ->
+                    ets:insert(?OPENERS, O#opener{clients=[From | Clients]}),
+                    {noreply, St};
+                [] ->
+                    Pid = ddoc_cache_entry:spawn_opener(OpenerKey),
+                    Opener = #opener{
+                        key = OpenerKey,
+                        pid = Pid,
+                        clients = [From]
+                    },
+                    ets:insert(?OPENERS, Opener),
+                    {noreply, St}
+            end;
+        [#entry{val = Val}] ->
+            {reply, {ok, Val}, St}
     end;
 
 handle_call(Msg, _From, St) ->
@@ -90,11 +97,11 @@ handle_call(Msg, _From, St) ->
 % The do_evict clauses are upgrades while we're
 % in a rolling reboot.
 handle_cast({do_evict, _} = Msg, St) ->
-    gen_server:cast(?LRU, Msg),
+    gen_server:cast(ddoc_cache_lru, Msg),
     {noreply, St};
 
-handle_cast({do_evict, _, _} = Msg, St) ->
-    gen_server:cast(?LRU, Msg),
+handle_cast({do_evict, DbName, DDocIds}, St) ->
+    gen_server:cast(ddoc_cache_lru, {do_refresh, DbName, DDocIds}),
     {noreply, St};
 
 handle_cast(Msg, St) ->
diff --git a/src/ddoc_cache/src/ddoc_cache_refresher.erl b/src/ddoc_cache/src/ddoc_cache_refresher.erl
index 8e8a6ef..ee2d644 100644
--- a/src/ddoc_cache/src/ddoc_cache_refresher.erl
+++ b/src/ddoc_cache/src/ddoc_cache_refresher.erl
@@ -11,107 +11,92 @@
 % the License.
 
 -module(ddoc_cache_refresher).
--behaviour(gen_server).
--vsn(1).
 
 
 -export([
-    spawn/1,
+    spawn_link/2,
+    refresh/1,
     stop/1
 ]).
 
+
 -export([
-    init/1,
-    terminate/2,
-    handle_call/3,
-    handle_cast/2,
-    handle_info/2,
-    code_change/3
+    init/1
 ]).
 
 
 -include("ddoc_cache.hrl").
 
 
--record(st, {
-    key
-}).
-
-
--define(REFRESH_TIMEOUT, 67000).
+spawn_link(Key, Interval) ->
+    proc_lib:spawn_link(?MODULE, init, [{self(), Key, Interval}]).
 
 
-spawn(Key) ->
-    proc_lib:spawn(?MODULE, init, [{self(), Key}]).
+refresh(Pid) ->
+    Pid ! refresh.
 
 
 stop(Pid) ->
-    gen_server:cast(Pid, stop).
+    unlink(Pid),
+    Pid ! stop.
 
 
-init({Parent, Key}) ->
-    process_flag(trap_exit, true),
+init({Parent, Key, Interval}) ->
     erlang:monitor(process, Parent),
-    gen_server:enter_loop(?MODULE, [], #st{key = Key}, ?REFRESH_TIMEOUT).
-
-
-terminate(_Reason, _St) ->
-    ok.
-
-
-handle_call(Msg, _From, St) ->
-    {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
-
-
-handle_cast(stop, St) ->
-    {stop, normal, St};
-
-handle_cast(Msg, St) ->
-    {stop, {invalid_cast, Msg}, St}.
-
-
-handle_info(timeout, St) ->
-    ddoc_cache_entry:spawn_link(St#st.key),
-    {noreply, St};
-
-handle_info({'EXIT', _, {open_ok, Key, Resp}}, #st{key = Key} = St) ->
-    Self = self(),
-    case Resp of
-        {ok, Val} ->
-            case ets:lookup(?CACHE, Key) of
-                [] ->
-                    % We were evicted
-                    {stop, normal, St};
-                [#entry{key = Key, val = Val, pid = Self}] ->
-                    % Value hasn't changed, do nothing
-                    {noreply, St};
-                [#entry{key = Key, pid = Self}] ->
-                    % Value changed, update cache
-                    case ddoc_cache_lru:refresh(Key, Val) of
-                        ok ->
-                            {noreply, St};
-                        evicted ->
-                            {stop, normal, St}
-                    end
-            end;
-        _Else ->
-            ddoc_cache_lru:remove(Key),
-            {stop, normal, St}
-    end;
-
-handle_info({'EXIT', _, _}, #st{key = Key} = St) ->
-    % Somethign went wrong trying to refresh the cache
-    % so bail in the interest of safety.
-    ddoc_cache_lru:remove(Key),
-    {stop, normal, St};
-
-handle_info({'DOWN', _, _, _, _}, St) ->
-    % ddoc_cache_lru died, so we will as well
-    {stop, normal, St};
-
-handle_info(Msg, St) ->
-    {stop, {invalid_info, Msg}, St}.
-
-
-code_change(_OldVsn, St, _Extra) ->
-    {ok, St}.
+    try
+        loop(Key, Interval)
+    catch T:R ->
+        S = erlang:get_stacktrace(),
+        exit({T, R, S})
+    end.
+
+
+loop(Key, Interval) ->
+    receive
+        refresh ->
+            do_refresh(Key, Interval);
+        stop ->
+            ok
+    after Interval ->
+        do_refresh(Key, Interval)
+    end.
+
+
+do_refresh(Key, Interval) ->
+    drain_refreshes(),
+    {_Pid, Ref} = ddoc_cache_entry:spawn_refresher(Key),
+    receive
+        {'DOWN', Ref, _, _, Resp} ->
+            case Resp of
+                {open_ok, Key, {ok, Val}} ->
+                    maybe_update(Key, Val, Interval);
+                _Else ->
+                    ddoc_cache_lru:remove(Key)
+            end
+    end.
+
+
+drain_refreshes() ->
+    receive
+        refresh ->
+            drain_refreshes()
+    after 0 ->
+        ok
+    end.
+
+
+maybe_update(Key, Val, Interval) ->
+    case ets:lookup(?CACHE, Key) of
+        [] ->
+            ok;
+        [#entry{val = Val}] ->
+            ?EVENT(update_noop, Key),
+            loop(Key, Interval);
+        [#entry{pid = Pid}] when Pid == self() ->
+            case ddoc_cache_lru:update(Key, Val) of
+                ok ->
+                    loop(Key, Interval);
+                evicted ->
+                    ok
+            end
+    end.
diff --git a/src/ddoc_cache/src/ddoc_cache_tables.erl b/src/ddoc_cache/src/ddoc_cache_tables.erl
index 2d57163..89aec6f 100644
--- a/src/ddoc_cache/src/ddoc_cache_tables.erl
+++ b/src/ddoc_cache/src/ddoc_cache_tables.erl
@@ -44,7 +44,7 @@ init(_) ->
         {keypos, #entry.key}
     ] ++ BaseOpts,
     ets:new(?CACHE, CacheOpts),
-    ets:new(?ATIMES, [ordered_set] ++ BaseOpts),
+    ets:new(?LRU, [ordered_set] ++ BaseOpts),
     ets:new(?OPENERS, [set, {keypos, #opener.key}] ++ BaseOpts),
     {ok, nil}.
 
diff --git a/src/ddoc_cache/src/ddoc_cache_util.erl b/src/ddoc_cache/src/ddoc_cache_util.erl
deleted file mode 100644
index fb3c0b9..0000000
--- a/src/ddoc_cache/src/ddoc_cache_util.erl
+++ /dev/null
@@ -1,34 +0,0 @@
-% 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_util).
-
-
--export([
-    new_uuid/0
-]).
-
-
-new_uuid() ->
-    to_hex(crypto:rand_bytes(16), []).
-
-
-to_hex(<<>>, Acc) ->
-    list_to_binary(lists:reverse(Acc));
-to_hex(<<C1:4, C2:4, Rest/binary>>, Acc) ->
-    to_hex(Rest, [hexdig(C1), hexdig(C2) | Acc]).
-
-
-hexdig(C) when C >= 0, C =< 9 ->
-    C + $0;
-hexdig(C) when C >= 10, C =< 15 ->
-    C + $A - 10.
diff --git a/src/ddoc_cache/test/ddoc_cache_basic_test.erl b/src/ddoc_cache/test/ddoc_cache_basic_test.erl
index ea114bd..c3b7760 100644
--- a/src/ddoc_cache/test/ddoc_cache_basic_test.erl
+++ b/src/ddoc_cache/test/ddoc_cache_basic_test.erl
@@ -38,7 +38,8 @@ check_basic_test_() ->
             fun cache_vdu/1,
             fun cache_custom/1,
             fun cache_ddoc_refresher_unchanged/1,
-            fun dont_cache_not_found/1
+            fun dont_cache_not_found/1,
+            fun deprecated_api_works/1
         ]}
     }.
 
@@ -108,4 +109,13 @@ dont_cache_not_found({DbName, _}) ->
     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)).
+    ?assertEqual(0, ets:info(?LRU, size)).
+
+
+deprecated_api_works({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open(DbName, ?FOOBAR),
+    {ok, _} = ddoc_cache:open(DbName, <<"foobar">>),
+    {ok, _} = ddoc_cache:open(DbName, ?MODULE),
+    {ok, _} = ddoc_cache:open(DbName, validation_funs).
+
diff --git a/src/ddoc_cache/test/ddoc_cache_coverage_test.erl b/src/ddoc_cache/test/ddoc_cache_coverage_test.erl
index 0cc4cdf..17e0770 100644
--- a/src/ddoc_cache/test/ddoc_cache_coverage_test.erl
+++ b/src/ddoc_cache/test/ddoc_cache_coverage_test.erl
@@ -18,7 +18,7 @@
 -include("ddoc_cache_test.hrl").
 
 
-check_basic_test_() ->
+coverage_test_() ->
     {
         setup,
         fun ddoc_cache_tutil:start_couch/0,
@@ -26,7 +26,9 @@ check_basic_test_() ->
         [
             fun restart_opener/0,
             fun restart_lru/0,
-            fun restart_tables/0
+            fun restart_tables/0,
+            fun restart_evictor/0,
+            fun lru_ignores_unknown_keys/0
         ]
     }.
 
@@ -52,6 +54,36 @@ restart_tables() ->
     ?assertEqual({ok, foo}, ddoc_cache_tables:code_change(1, foo, [])).
 
 
+restart_evictor() ->
+    meck:new(ddoc_cache_ev, [passthrough]),
+    try
+        State = sys:get_state(ddoc_cache_lru),
+        Evictor = element(5, State),
+        Ref = erlang:monitor(process, Evictor),
+        exit(Evictor, shutdown),
+        receive
+            {'DOWN', Ref, _, _, Reason} ->
+                couch_log:error("MONITOR: ~p", [Reason]),
+                ok
+        end,
+        meck:wait(ddoc_cache_ev, event, [evictor_died, '_'], 1000),
+        NewState = sys:get_state(ddoc_cache_lru),
+        NewEvictor = element(5, NewState),
+        ?assertNotEqual(Evictor, NewEvictor)
+    after
+        meck:unload()
+    end.
+
+
+lru_ignores_unknown_keys() ->
+    ?assertEqual(evicted, gen_server:call(ddoc_cache_lru, {update, foo, bar})),
+    ?assertEqual(ok, gen_server:call(ddoc_cache_lru, {remove, foo})),
+    Pid = whereis(ddoc_cache_lru),
+    gen_server:cast(ddoc_cache_lru, {accessed, foo}),
+    timer:sleep(200),
+    ?assert(is_process_alive(Pid)).
+
+
 send_bad_messages(Name) ->
     wait_for_restart(Name, fun() ->
         ?assertEqual({invalid_call, foo}, gen_server:call(Name, foo))
diff --git a/src/ddoc_cache/test/ddoc_cache_disabled_test.erl b/src/ddoc_cache/test/ddoc_cache_disabled_test.erl
index 3e412c7..ef73180 100644
--- a/src/ddoc_cache/test/ddoc_cache_disabled_test.erl
+++ b/src/ddoc_cache/test/ddoc_cache_disabled_test.erl
@@ -41,7 +41,7 @@ resp_ok({DbName, _}) ->
     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)).
+    ?assertEqual(0, ets:info(?LRU, size)).
 
 
 resp_not_found({DbName, _}) ->
@@ -49,4 +49,4 @@ resp_not_found({DbName, _}) ->
     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)).
+    ?assertEqual(0, ets:info(?LRU, size)).
diff --git a/src/ddoc_cache/test/ddoc_cache_ev.erl b/src/ddoc_cache/test/ddoc_cache_ev.erl
index a9ea475..a451342 100644
--- a/src/ddoc_cache/test/ddoc_cache_ev.erl
+++ b/src/ddoc_cache/test/ddoc_cache_ev.erl
@@ -13,10 +13,9 @@
 -module(ddoc_cache_ev).
 
 -export([
-    evicted/2
+    event/2
 ]).
 
 
-evicted(_DbName, _DDocIds) ->
-    couch_log:error("EVICTION EVENT: ~p ~p~n", [_DbName, _DDocIds]),
-    ok.
+event(Name, Arg) ->
+    couch_log:error("~s :: ~s :: ~p", [?MODULE, Name, Arg]).
diff --git a/src/ddoc_cache/test/ddoc_cache_eviction_test.erl b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl
index d36157d..aeca337 100644
--- a/src/ddoc_cache/test/ddoc_cache_eviction_test.erl
+++ b/src/ddoc_cache/test/ddoc_cache_eviction_test.erl
@@ -44,77 +44,16 @@ check_eviction_test_() ->
         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
+            fun check_upgrade_clause/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(),
+    meck:reset(ddoc_cache_ev),
     Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
     ShardName = element(2, hd(mem3:shards(DbName))),
     {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
@@ -123,22 +62,13 @@ evict_all({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),
+    meck:wait(ddoc_cache_ev, event, [evicted, DbName], 1000),
     ?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(),
+    meck:reset(ddoc_cache_ev),
     Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
     {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
     {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
@@ -147,34 +77,15 @@ dont_evict_all_unrelated({DbName, _}) ->
     ?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),
+    meck:wait(ddoc_cache_ev, event, [evict_noop, <<"test">>], 1000),
     ?assertEqual(4, ets:info(?CACHE, size)).
 
 
-check_upgrade_clauses({DbName, _}) ->
+check_upgrade_clause({DbName, _}) ->
     ddoc_cache_tutil:clear(),
-    ShardName = element(2, hd(mem3:shards(DbName))),
+    meck:reset(ddoc_cache_ev),
     {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).
+    meck:wait(ddoc_cache_ev, event, [evicted, DbName], 1000),
+    ?assertEqual(0, ets:info(?CACHE, size)).
diff --git a/src/ddoc_cache/test/ddoc_cache_opener_test.erl b/src/ddoc_cache/test/ddoc_cache_opener_test.erl
new file mode 100644
index 0000000..5ea4b0f
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_opener_test.erl
@@ -0,0 +1,76 @@
+% 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_opener_test).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+opener_test_() ->
+    {
+        setup,
+        fun ddoc_cache_tutil:start_couch/0,
+        fun ddoc_cache_tutil:stop_couch/1,
+        {with, [
+            fun check_multiple/1,
+            fun handles_opened/1,
+            fun handles_error/1
+        ]}
+    }.
+
+
+check_multiple({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    % We're faking multiple concurrent readers by pausing the
+    % ddoc_cache_opener process, sending it a few messages
+    % and then resuming the process.
+    Pid = whereis(ddoc_cache_opener),
+    Key = {ddoc_cache_entry_ddocid, {DbName, ?FOOBAR}},
+    erlang:suspend_process(Pid),
+    lists:foreach(fun(_) ->
+        Pid ! {'$gen_call', {self(), make_ref()}, {open, Key}}
+    end, lists:seq(1, 10)),
+    erlang:resume_process(Pid),
+    lists:foreach(fun(_) ->
+        receive
+            {_, {open_ok, _, _}} -> ok
+        end
+    end, lists:seq(1, 10)).
+
+
+handles_opened({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    [#entry{key = Key, val = Val}] = ets:tab2list(?CACHE),
+    Resp = gen_server:call(ddoc_cache_opener, {open, Key}),
+    ?assertEqual({ok, Val}, Resp).
+
+
+handles_error({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:new(ddoc_cache_entry, [passthrough]),
+    meck:expect(ddoc_cache_entry, do_open, fun(_, _) ->
+        couch_log:error("OHAI", []),
+        erlang:error(borkity)
+    end),
+    try
+        ?assertError(
+                {ddoc_cache_error, _},
+                ddoc_cache:open_doc(DbName, ?FOOBAR)
+            )
+    after
+        meck:unload()
+    end.
+
diff --git a/src/ddoc_cache/test/ddoc_cache_refresh_test.erl b/src/ddoc_cache/test/ddoc_cache_refresh_test.erl
new file mode 100644
index 0000000..7cdcff5
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_refresh_test.erl
@@ -0,0 +1,167 @@
+% 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_refresh_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, rand_string()}}.
+
+
+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_refresh_test_() ->
+    {
+        setup,
+        fun start_couch/0,
+        fun stop_couch/1,
+        {with, [
+            fun refresh_ddoc/1,
+            fun refresh_ddoc_rev/1,
+            fun refresh_vdu/1,
+            fun refresh_custom/1,
+            fun refresh_multiple/1,
+            fun check_upgrade_clause/1
+        ]}
+    }.
+
+
+refresh_ddoc({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"foo">>, <<"baz">>}]}
+    },
+    {ok, {Depth, RevId}} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    Expect = NewDDoc#doc{
+        revs = {Depth, [RevId | element(2, DDoc#doc.revs)]}
+    },
+    meck:wait(ddoc_cache_ev, event, [updated, {Key, Expect}], 1000),
+    ?assertMatch({ok, Expect}, ddoc_cache:open_doc(DbName, ?FOOBAR)),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+refresh_ddoc_rev({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    {ok, RevDDoc} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"foo">>, <<"kazam">>}]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    % We pass the rev explicitly so we assert that we're
+    % getting the same original response from the cache
+    meck:wait(ddoc_cache_ev, event, [update_noop, Key], 1000),
+    ?assertMatch({ok, RevDDoc}, ddoc_cache:open_doc(DbName, ?FOOBAR, Rev)),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+refresh_vdu({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    {ok, [_]} = ddoc_cache:open_validation_funs(DbName),
+    [#entry{key = Key}] = ets:tab2list(?CACHE),
+    {ok, DDoc} = fabric:open_doc(DbName, ?VDU, [?ADMIN_CTX]),
+    {ok, _} = fabric:update_doc(DbName, DDoc#doc{body = {[]}}, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [updated, {Key, []}], 1000),
+    ?assertMatch({ok, []}, ddoc_cache:open_validation_funs(DbName)),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+refresh_custom({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    {ok, Resp1} = ddoc_cache:open_custom(DbName, ?MODULE),
+    {ok, DDoc} = fabric:open_doc(DbName, ?VDU, [?CUSTOM]),
+    {ok, _} = fabric:update_doc(DbName, DDoc#doc{body = {[]}}, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [updated, '_'], 1000),
+    ?assertNotEqual({ok, Resp1}, ddoc_cache:open_custom(DbName, ?MODULE)),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+
+refresh_multiple({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?FOOBAR),
+    {ok, DDoc} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    {ok, DDoc} = ddoc_cache:open_doc(DbName, ?FOOBAR, Rev),
+    ?assertEqual(2, ets:info(?CACHE, size)),
+    % Relying on the sort order of entry keys to make
+    % sure our entries line up for this test
+    [
+        #entry{key = NoRevKey, val = DDoc},
+        #entry{key = RevKey, val = DDoc}
+    ] = lists:sort(ets:tab2list(?CACHE)),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"foo">>, <<"kalamazoo">>}]}
+    },
+    {ok, {Depth, RevId}} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    Updated = NewDDoc#doc{
+        revs = {Depth, [RevId | element(2, DDoc#doc.revs)]}
+    },
+    meck:wait(ddoc_cache_ev, event, [update_noop, RevKey], 1000),
+    meck:wait(ddoc_cache_ev, event, [updated, {NoRevKey, Updated}], 1000),
+    % We pass the rev explicitly so we assert that we're
+    % getting the same original response from the cache
+    ?assertEqual({ok, Updated}, ddoc_cache:open_doc(DbName, ?FOOBAR)),
+    ?assertEqual({ok, DDoc}, ddoc_cache:open_doc(DbName, ?FOOBAR, Rev)),
+    ?assertEqual(2, ets:info(?CACHE, size)).
+
+
+check_upgrade_clause({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    [#entry{key = Key}] = ets:tab2list(?CACHE),
+    gen_server:cast(ddoc_cache_opener, {do_evict, DbName, [?FOOBAR]}),
+    meck:wait(ddoc_cache_ev, event, [update_noop, Key], 1000).
+
+
+rand_string() ->
+    Bin = crypto:rand_bytes(8),
+    to_hex(Bin, []).
+
+
+to_hex(<<>>, Acc) ->
+    list_to_binary(lists:reverse(Acc));
+to_hex(<<C1:4, C2:4, Rest/binary>>, Acc) ->
+    to_hex(Rest, [hexdig(C1), hexdig(C2) | Acc]).
+
+
+hexdig(C) when C >= 0, C =< 9 ->
+    C + $0;
+hexdig(C) when C >= 10, C =< 15 ->
+    C + $A - 10.
\ No newline at end of file
diff --git a/src/ddoc_cache/test/ddoc_cache_refresher_test.erl b/src/ddoc_cache/test/ddoc_cache_refresher_test.erl
new file mode 100644
index 0000000..5c3e107
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_refresher_test.erl
@@ -0,0 +1,164 @@
+% 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_refresher_test).
+
+
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+key() ->
+    {ddoc_cache_entry_custom, {<<"dbname_here">>, ?MODULE}}.
+
+
+setup() ->
+    ets:new(?CACHE, [public, named_table, set, {keypos, #entry.key}]),
+    ets:insert(?CACHE, #entry{key = key(), val = bang}),
+    meck:new(ddoc_cache_lru, [passthrough]),
+    meck:new(ddoc_cache_entry, [passthrough]).
+
+
+teardown(_) ->
+    meck:unload(),
+    ets:delete(?CACHE).
+
+
+refresher_test_() ->
+    {
+        foreach,
+        fun setup/0,
+        fun teardown/1,
+        [
+            fun handles_error/0,
+            fun refresh_on_timeout/0,
+            fun refresh_once/0,
+            fun dies_on_missing_entry/0,
+            fun dies_on_evicted/0
+        ]
+    }.
+
+
+handles_error() ->
+    meck:expect(ddoc_cache_entry, spawn_refresher, fun(_) ->
+        throw(foo)
+    end),
+    {Pid, Ref} = spawn_refresher(),
+    ddoc_cache_refresher:refresh(Pid),
+    receive
+        {'DOWN', Ref, _, _, {throw, foo, _}} ->
+            ok
+    end.
+
+
+refresh_on_timeout() ->
+    meck:expect(ddoc_cache_entry, spawn_refresher, fun(Key) ->
+        Ref = erlang:make_ref(),
+        self() ! {'DOWN', Ref, process, pid, {open_ok, Key, {ok, zot}}},
+        {self(), Ref}
+    end),
+    meck:expect(ddoc_cache_lru, update, fun(_, zot) ->
+        ok
+    end),
+    {Pid, _} = spawn_refresher(),
+    ets:update_element(?CACHE, key(), {#entry.pid, Pid}),
+    % This is the assertion that if we wait long enough
+    % the update will be called.
+    meck:wait(ddoc_cache_lru, update, ['_', '_'], 1000),
+    ?assert(is_process_alive(Pid)),
+    ddoc_cache_refresher:stop(Pid).
+
+
+refresh_once() ->
+    Counter = spawn_counter(),
+    meck:expect(ddoc_cache_entry, spawn_refresher, fun(Key) ->
+        Ref = erlang:make_ref(),
+        Count = get_count(Counter),
+        self() ! {'DOWN', Ref, process, pid, {open_ok, Key, {ok, Count}}},
+        {self(), Ref}
+    end),
+    meck:expect(ddoc_cache_lru, update, fun(_, 1) ->
+        ok
+    end),
+    {Pid, _} = spawn_refresher(),
+    ets:update_element(?CACHE, key(), {#entry.pid, Pid}),
+    erlang:suspend_process(Pid),
+    lists:foreach(fun(_) ->
+        ddoc_cache_refresher:refresh(Pid)
+    end, lists:seq(1, 100)),
+    erlang:resume_process(Pid),
+    % This is the assertion that if we wait long enough
+    % the update will be called.
+    meck:wait(ddoc_cache_lru, update, ['_', '_'], 1000),
+    ?assert(is_process_alive(Pid)),
+    ddoc_cache_refresher:stop(Pid).
+
+
+dies_on_missing_entry() ->
+    meck:expect(ddoc_cache_entry, spawn_refresher, fun(Key) ->
+        Ref = erlang:make_ref(),
+        self() ! {'DOWN', Ref, process, pid, {open_ok, Key, {ok, zot}}},
+        {self(), Ref}
+    end),
+    {Pid, _} = spawn_refresher(),
+    ets:delete(?CACHE, key()),
+    ddoc_cache_refresher:refresh(Pid),
+    receive
+        {'DOWN', _, _, Pid, normal} ->
+            ok
+    end.
+
+
+dies_on_evicted() ->
+    meck:expect(ddoc_cache_entry, spawn_refresher, fun(Key) ->
+        Ref = erlang:make_ref(),
+        self() ! {'DOWN', Ref, process, pid, {open_ok, Key, {ok, zot}}},
+        {self(), Ref}
+    end),
+    meck:expect(ddoc_cache_lru, update, fun(_, zot) ->
+        evicted
+    end),
+    {Pid, _} = spawn_refresher(),
+    ets:update_element(?CACHE, key(), {#entry.pid, Pid}),
+    ddoc_cache_refresher:refresh(Pid),
+    receive
+        {'DOWN', _, _, Pid, normal} ->
+            ok
+    end.
+
+
+spawn_refresher() ->
+    erlang:spawn_monitor(ddoc_cache_refresher, init, [{self(), key(), 100}]).
+
+
+spawn_counter() ->
+    erlang:spawn_link(fun do_counting/0).
+
+
+get_count(Pid) ->
+    Pid ! {get, self()},
+    receive
+        {count, Pid, N} ->
+            N
+    end.
+
+
+do_counting() ->
+    do_counting(0).
+
+
+do_counting(N) ->
+    receive
+        {get, From} ->
+            From ! {count, self(), N + 1},
+            do_counting(N + 1)
+    end.
\ No newline at end of file
diff --git a/src/ddoc_cache/test/ddoc_cache_remove_test.erl b/src/ddoc_cache/test/ddoc_cache_remove_test.erl
new file mode 100644
index 0000000..8c081ba
--- /dev/null
+++ b/src/ddoc_cache/test/ddoc_cache_remove_test.erl
@@ -0,0 +1,205 @@
+% 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_remove_test).
+
+
+-export([
+    recover/1
+]).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("mem3/include/mem3.hrl").
+-include_lib("eunit/include/eunit.hrl").
+-include("ddoc_cache_test.hrl").
+
+
+recover(DbName) ->
+    {ok, #doc{body = {Body}}} = fabric:open_doc(DbName, ?CUSTOM, [?ADMIN_CTX]),
+    case couch_util:get_value(<<"status">>, Body) of
+        <<"ok">> ->
+            {ok, yay};
+        <<"not_ok">> ->
+            {ruh, roh};
+        <<"error">> ->
+            erlang:error(thpppt)
+    end.
+
+
+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_refresh_test_() ->
+    {
+        setup,
+        fun start_couch/0,
+        fun stop_couch/1,
+        {with, [
+            fun remove_ddoc/1,
+            fun remove_ddoc_rev/1,
+            fun remove_ddoc_rev_only/1,
+            fun remove_custom_not_ok/1,
+            fun remove_custom_error/1
+        ]}
+    }.
+
+
+remove_ddoc({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?FOOBAR),
+    ?assertEqual(1, ets:info(?CACHE, size)),
+    [#entry{key = Key, val = DDoc}] = ets:tab2list(?CACHE),
+    NewDDoc = DDoc#doc{
+        deleted = true,
+        body = {[]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [removed, Key], 1000),
+    ?assertMatch({not_found, deleted}, ddoc_cache:open_doc(DbName, ?FOOBAR)),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+remove_ddoc_rev({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?VDU),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?VDU, Rev),
+    [#entry{key = Key, val = DDoc, pid = Pid}] = ets:tab2list(?CACHE),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"an">>, <<"update">>}]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [update_noop, Key], 1000),
+    % Compact the database so that the old rev is removed
+    lists:foreach(fun(Shard) ->
+        do_compact(Shard#shard.name)
+    end, mem3:local_shards(DbName)),
+    % Trigger a refresh rather than wait for the timeout
+    ddoc_cache_refresher:refresh(Pid),
+    meck:wait(ddoc_cache_ev, event, [removed, Key], 1000),
+    ?assertMatch(
+            {{not_found, missing}, _},
+            ddoc_cache:open_doc(DbName, ?VDU, Rev)
+        ),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+remove_ddoc_rev_only({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    Rev = ddoc_cache_tutil:get_rev(DbName, ?VDU),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?VDU),
+    {ok, _} = ddoc_cache:open_doc(DbName, ?VDU, Rev),
+    % Relying on the sort order of keys to keep
+    % these lined up for testing
+    [
+        #entry{key = NoRevKey, val = DDoc, pid = NoRevPid},
+        #entry{key = RevKey, val = DDoc, pid = RevPid}
+    ] = lists:sort(ets:tab2list(?CACHE)),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"new">>, <<"awesomeness">>}]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [updated, '_'], 1000),
+    meck:wait(ddoc_cache_ev, event, [update_noop, RevKey], 1000),
+    % Compact the database so that the old rev is removed
+    lists:foreach(fun(Shard) ->
+        do_compact(Shard#shard.name)
+    end, mem3:local_shards(DbName)),
+    % Trigger a refresh rather than wait for the timeout
+    ddoc_cache_refresher:refresh(NoRevPid),
+    ddoc_cache_refresher:refresh(RevPid),
+    meck:wait(ddoc_cache_ev, event, [update_noop, NoRevKey], 1000),
+    meck:wait(ddoc_cache_ev, event, [removed, RevKey], 1000),
+    ?assertMatch({ok, _}, ddoc_cache:open_doc(DbName, ?VDU)),
+    ?assertMatch(
+            {{not_found, missing}, _},
+            ddoc_cache:open_doc(DbName, ?VDU, Rev)
+        ),
+    ?assertEqual(1, ets:info(?CACHE, size)).
+
+remove_custom_not_ok({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    init_custom_ddoc(DbName),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    [#entry{key = Key}] = ets:tab2list(?CACHE),
+    {ok, DDoc} = fabric:open_doc(DbName, ?CUSTOM, [?ADMIN_CTX]),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"status">>, <<"not_ok">>}]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [removed, Key], 1000),
+    ?assertEqual({ruh, roh}, ddoc_cache:open_custom(DbName, ?MODULE)),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+remove_custom_error({DbName, _}) ->
+    ddoc_cache_tutil:clear(),
+    meck:reset(ddoc_cache_ev),
+    init_custom_ddoc(DbName),
+    {ok, _} = ddoc_cache:open_custom(DbName, ?MODULE),
+    [#entry{key = Key}] = ets:tab2list(?CACHE),
+    {ok, DDoc} = fabric:open_doc(DbName, ?CUSTOM, [?ADMIN_CTX]),
+    NewDDoc = DDoc#doc{
+        body = {[{<<"status">>, <<"error">>}]}
+    },
+    {ok, _} = fabric:update_doc(DbName, NewDDoc, [?ADMIN_CTX]),
+    meck:wait(ddoc_cache_ev, event, [removed, Key], 1000),
+    ?assertError(thpppt, ddoc_cache:open_custom(DbName, ?MODULE)),
+    ?assertEqual(0, ets:info(?CACHE, size)).
+
+
+init_custom_ddoc(DbName) ->
+    Body = {[{<<"status">>, <<"ok">>}]},
+    {ok, Doc} = fabric:open_doc(DbName, ?CUSTOM, [?ADMIN_CTX]),
+    NewDoc = Doc#doc{body = Body},
+    {ok, _} = fabric:update_doc(DbName, NewDoc, [?ADMIN_CTX]).
+
+
+do_compact(ShardName) ->
+    {ok, Db} = couch_db:open_int(ShardName, []),
+    try
+        {ok, Pid} = couch_db:start_compact(Db),
+        Ref = erlang:monitor(process, Pid),
+        receive
+            {'DOWN', Ref, _, _, _} ->
+                ok
+        end
+    after
+        couch_db:close(Db)
+    end,
+    wait_for_compaction(ShardName).
+
+
+wait_for_compaction(ShardName) ->
+    {ok, Db} = couch_db:open_int(ShardName, []),
+    CompactRunning = try
+        {ok, Info} = couch_db:get_db_info(Db),
+        couch_util:get_value(compact_running, Info)
+    after
+        couch_db:close(Db)
+    end,
+    if not CompactRunning -> ok; true ->
+        timer:sleep(100),
+        wait_for_compaction(ShardName)
+    end.
\ No newline at end of file
diff --git a/src/ddoc_cache/test/ddoc_cache_test.hrl b/src/ddoc_cache/test/ddoc_cache_test.hrl
index 1a09804..73f7bc2 100644
--- a/src/ddoc_cache/test/ddoc_cache_test.hrl
+++ b/src/ddoc_cache/test/ddoc_cache_test.hrl
@@ -12,9 +12,15 @@
 
 
 -define(CACHE, ddoc_cache_entries).
--define(ATIMES, ddoc_cache_atimes).
+-define(LRU, ddoc_cache_lru).
 -define(OPENERS, ddoc_cache_openers).
 
 -define(FOOBAR, <<"_design/foobar">>).
 -define(VDU, <<"_design/vdu">>).
--define(CUSTOM, <<"_design/custom">>).
\ No newline at end of file
+-define(CUSTOM, <<"_design/custom">>).
+
+-record(entry, {
+    key,
+    val,
+    pid
+}).
diff --git a/src/ddoc_cache/test/ddoc_cache_tutil.erl b/src/ddoc_cache/test/ddoc_cache_tutil.erl
index 0ac68fa..8321dd5 100644
--- a/src/ddoc_cache/test/ddoc_cache_tutil.erl
+++ b/src/ddoc_cache/test/ddoc_cache_tutil.erl
@@ -40,7 +40,7 @@ clear() ->
 get_rev(DbName, DDocId) ->
     {_, Ref} = erlang:spawn_monitor(fun() ->
         {ok, #doc{revs = Revs}} = fabric:open_doc(DbName, DDocId, [?ADMIN_CTX]),
-        {Depth, [RevId]} = Revs,
+        {Depth, [RevId | _]} = Revs,
         exit({Depth, RevId})
     end),
     receive
@@ -64,6 +64,7 @@ ddocs() ->
     Custom = #doc{
         id = <<"_design/custom">>,
         body = {[
+            {<<"status">>, <<"ok">>},
             {<<"custom">>, <<"hotrod">>}
         ]}
     },

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

Mime
View raw message